maltose1 commited on
Commit
d498971
·
verified ·
1 Parent(s): 733a08d

Upload 9 files

Browse files
Files changed (4) hide show
  1. README.md +6 -1
  2. getCaptcha.py +28 -28
  3. main.py +698 -687
  4. resigner.py +140 -134
README.md CHANGED
@@ -55,7 +55,12 @@ curl "https://huggingface.co/spaces/YOUR_USERNAME/YOUR_SPACE_NAME/v1/chat/comple
55
  - **格式**: 使用逗号分隔。
56
  - **示例**: `sk-mykey-1,sk-mykey-2`
57
 
58
- 3. **`DEBUG_MODE`** (可选)
 
 
 
 
 
59
  - **说明**: 设置为 `true` 可以启用详细的调试日志。
60
  - **格式**: `true` 或 `false`。
61
  - **示例**: `true`
 
55
  - **格式**: 使用逗号分隔。
56
  - **示例**: `sk-mykey-1,sk-mykey-2`
57
 
58
+ 3. **`HTTP_PROXY`** (可选)
59
+ - **说明**: 为所有出站请求(例如到 Tenbin API)设置 HTTP/HTTPS 代理。
60
+ - **格式**: 标准的代理 URL。
61
+ - **示例**: `http://user:pass@host:port`
62
+
63
+ 4. **`DEBUG_MODE`** (可选)
64
  - **说明**: 设置为 `true` 可以启用详细的调试日志。
65
  - **格式**: `true` 或 `false`。
66
  - **示例**: `true`
getCaptcha.py CHANGED
@@ -1,29 +1,29 @@
1
- import requests
2
- import time
3
- import os
4
-
5
- TURNSTILE_SOLVER_URL = os.environ.get("TURNSTILE_SOLVER_URL", "http://127.0.0.1:5000")
6
-
7
- def getTaskId():
8
- url = f"{TURNSTILE_SOLVER_URL}/turnstile?url=https://tenbin.ai/workspace&sitekey=0x4AAAAAABGR2exxRproizri&action=issue_execution_token"
9
-
10
- response = requests.get(url)
11
- response.raise_for_status()
12
- return response.json()['task_id']
13
-
14
- def getCaptcha(task_id):
15
-
16
- url = f"{TURNSTILE_SOLVER_URL}/result?id={task_id}"
17
-
18
- while True:
19
- try:
20
- response = requests.get(url)
21
- response.raise_for_status()
22
- captcha = response.json().get('value', None)
23
- if captcha:
24
- return captcha
25
- else:
26
- time.sleep(1)
27
- except Exception as e:
28
- print(e)
29
  time.sleep(1)
 
1
+ import requests
2
+ import time
3
+ import os
4
+
5
+ TURNSTILE_SOLVER_URL = os.environ.get("TURNSTILE_SOLVER_URL", "http://127.0.0.1:5000")
6
+
7
+ def getTaskId():
8
+ url = f"{TURNSTILE_SOLVER_URL}/turnstile?url=https://tenbin.ai/workspace&sitekey=0x4AAAAAABGR2exxRproizri&action=issue_execution_token"
9
+
10
+ response = requests.get(url)
11
+ response.raise_for_status()
12
+ return response.json()['task_id']
13
+
14
+ def getCaptcha(task_id):
15
+
16
+ url = f"{TURNSTILE_SOLVER_URL}/result?id={task_id}"
17
+
18
+ while True:
19
+ try:
20
+ response = requests.get(url)
21
+ response.raise_for_status()
22
+ captcha = response.json().get('value', None)
23
+ if captcha:
24
+ return captcha
25
+ else:
26
+ time.sleep(1)
27
+ except Exception as e:
28
+ print(e)
29
  time.sleep(1)
main.py CHANGED
@@ -1,688 +1,699 @@
1
- import json
2
- import os
3
- import time
4
- import uuid
5
- import threading
6
- from typing import Any, Dict, List, Optional, TypedDict, Union
7
-
8
- import requests
9
- import websocket
10
- from fastapi import FastAPI, HTTPException, Depends, Query
11
- from fastapi.responses import StreamingResponse
12
- from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
13
- from pydantic import BaseModel, Field
14
-
15
- from getCaptcha import getCaptcha, getTaskId
16
-
17
-
18
- # Tenbin Account Management
19
- class TenbinAccount(TypedDict):
20
- session_id: str
21
- is_valid: bool
22
- last_used: float
23
- error_count: int
24
-
25
-
26
- # Global variables
27
- VALID_CLIENT_KEYS: set = set()
28
- TENBIN_ACCOUNTS: List[TenbinAccount] = []
29
- TENBIN_MODELS: Dict[str, str] = {} # 模型映射表,key 是模型名称,value 是内部模型 ID
30
- account_rotation_lock = threading.Lock()
31
- MAX_ERROR_COUNT = 3
32
- ERROR_COOLDOWN = 300 # 5 minutes cooldown for accounts with errors
33
- DEBUG_MODE = os.environ.get("DEBUG_MODE", "false").lower() == "true"
34
- REQUEST_TIMEOUT = 120.0 # 请求超时时间,秒
35
-
36
-
37
- # Pydantic Models
38
- class ChatMessage(BaseModel):
39
- role: str
40
- content: Union[str, List[Dict[str, Any]]]
41
- reasoning_content: Optional[str] = None
42
-
43
-
44
- class ChatCompletionRequest(BaseModel):
45
- model: str
46
- messages: List[ChatMessage]
47
- stream: bool = True
48
- temperature: Optional[float] = None
49
- max_tokens: Optional[int] = None
50
- top_p: Optional[float] = None
51
- raw_response: bool = False # 是否返回原始响应
52
-
53
-
54
- class ModelInfo(BaseModel):
55
- id: str
56
- object: str = "model"
57
- created: int
58
- owned_by: str = "tenbin"
59
-
60
-
61
- class ModelList(BaseModel):
62
- object: str = "list"
63
- data: List[ModelInfo]
64
-
65
-
66
- class ChatCompletionChoice(BaseModel):
67
- message: ChatMessage
68
- index: int = 0
69
- finish_reason: str = "stop"
70
-
71
-
72
- class ChatCompletionResponse(BaseModel):
73
- id: str = Field(default_factory=lambda: f"chatcmpl-{uuid.uuid4().hex}")
74
- object: str = "chat.completion"
75
- created: int = Field(default_factory=lambda: int(time.time()))
76
- model: str
77
- choices: List[ChatCompletionChoice]
78
- usage: Dict[str, int] = Field(
79
- default_factory=lambda: {
80
- "prompt_tokens": 0,
81
- "completion_tokens": 0,
82
- "total_tokens": 0,
83
- }
84
- )
85
-
86
-
87
- class StreamChoice(BaseModel):
88
- delta: Dict[str, Any] = Field(default_factory=dict)
89
- index: int = 0
90
- finish_reason: Optional[str] = None
91
-
92
-
93
- class StreamResponse(BaseModel):
94
- id: str = Field(default_factory=lambda: f"chatcmpl-{uuid.uuid4().hex}")
95
- object: str = "chat.completion.chunk"
96
- created: int = Field(default_factory=lambda: int(time.time()))
97
- model: str
98
- choices: List[StreamChoice]
99
-
100
-
101
- # FastAPI App
102
- app = FastAPI(title="Tenbin OpenAI API Adapter")
103
- security = HTTPBearer(auto_error=False)
104
-
105
-
106
- def log_debug(message: str):
107
- """Debug日志函数"""
108
- if DEBUG_MODE:
109
- print(f"[DEBUG] {message}")
110
-
111
-
112
- def load_client_api_keys():
113
- """Load client API keys from environment variable (comma-separated) or client_api_keys.json"""
114
- global VALID_CLIENT_KEYS
115
- keys_str = os.environ.get("API_KEYS")
116
- try:
117
- if keys_str:
118
- print("Loading client API keys from API_KEYS environment variable.")
119
- keys = [key.strip() for key in keys_str.split(',')]
120
- else:
121
- print("Loading client API keys from file: client_api_keys.json")
122
- with open("client_api_keys.json", "r", encoding="utf-8") as f:
123
- keys = json.load(f)
124
-
125
- VALID_CLIENT_KEYS = set(keys) if isinstance(keys, list) else set()
126
- print(f"Successfully loaded {len(VALID_CLIENT_KEYS)} client API keys.")
127
- except FileNotFoundError:
128
- print("Error: client_api_keys.json not found and API_KEYS not set. Client authentication will fail.")
129
- VALID_CLIENT_KEYS = set()
130
- except Exception as e:
131
- print(f"Error loading client API keys: {e}")
132
- VALID_CLIENT_KEYS = set()
133
-
134
-
135
- def load_tenbin_accounts():
136
- """Load Tenbin accounts from environment variable (comma-separated session_ids) or tenbin.json"""
137
- global TENBIN_ACCOUNTS
138
- TENBIN_ACCOUNTS = []
139
- session_ids_str = os.environ.get("SESSION_IDS")
140
- try:
141
- accounts_to_process = []
142
- if session_ids_str:
143
- print("Loading Tenbin accounts from SESSION_IDS environment variable.")
144
- session_ids = [sid.strip() for sid in session_ids_str.split(',')]
145
- accounts_to_process = [{"session_id": sid} for sid in session_ids]
146
- else:
147
- print("Loading Tenbin accounts from file: tenbin.json")
148
- with open("tenbin.json", "r", encoding="utf-8") as f:
149
- accounts_to_process = json.load(f)
150
-
151
- if not isinstance(accounts_to_process, list):
152
- print("Warning: Account data should be a list of objects.")
153
- return
154
-
155
- for acc in accounts_to_process:
156
- session_id = acc.get("session_id")
157
- if session_id:
158
- TENBIN_ACCOUNTS.append({
159
- "session_id": session_id,
160
- "is_valid": True,
161
- "last_used": 0,
162
- "error_count": 0
163
- })
164
- print(f"Successfully loaded {len(TENBIN_ACCOUNTS)} Tenbin accounts.")
165
- except FileNotFoundError:
166
- print("Error: tenbin.json not found and SESSION_IDS not set. API calls will fail.")
167
- except Exception as e:
168
- print(f"Error loading tenbin.json: {e}")
169
-
170
-
171
- def load_tenbin_models():
172
- """Load Tenbin models from models.json"""
173
- global TENBIN_MODELS
174
- try:
175
- with open("models.json", "r", encoding="utf-8") as f:
176
- models_data = json.load(f)
177
- if isinstance(models_data, dict):
178
- TENBIN_MODELS = models_data
179
- print(f"Successfully loaded {len(TENBIN_MODELS)} models.")
180
- else:
181
- print("Warning: models.json should contain a dictionary of model mappings.")
182
- TENBIN_MODELS = {}
183
- except FileNotFoundError:
184
- print("Error: models.json not found. Model list will be empty.")
185
- TENBIN_MODELS = {}
186
- except Exception as e:
187
- print(f"Error loading models.json: {e}")
188
- TENBIN_MODELS = {}
189
-
190
-
191
- def get_best_tenbin_account() -> Optional[TenbinAccount]:
192
- """Get the best available Tenbin account using a smart selection algorithm."""
193
- with account_rotation_lock:
194
- now = time.time()
195
- valid_accounts = [
196
- acc for acc in TENBIN_ACCOUNTS
197
- if acc["is_valid"] and (
198
- acc["error_count"] < MAX_ERROR_COUNT or
199
- now - acc["last_used"] > ERROR_COOLDOWN
200
- )
201
- ]
202
-
203
- if not valid_accounts:
204
- return None
205
-
206
- # Reset error count for accounts that have been in cooldown
207
- for acc in valid_accounts:
208
- if acc["error_count"] >= MAX_ERROR_COUNT and now - acc["last_used"] > ERROR_COOLDOWN:
209
- acc["error_count"] = 0
210
-
211
- # Sort by last used (oldest first) and error count (lowest first)
212
- valid_accounts.sort(key=lambda x: (x["last_used"], x["error_count"]))
213
- account = valid_accounts[0]
214
- account["last_used"] = now
215
- return account
216
-
217
-
218
- def build_tenbin_prompt(messages: List[ChatMessage]) -> str:
219
- """将 OpenAI 格式的消息列表转换为 Tenbin 格式的单个字符串"""
220
- prompt = ""
221
- for msg in messages:
222
- role = msg.role
223
- content = msg.content
224
- if isinstance(content, list):
225
- # 简单处理多模态内容,只提取文本部分
226
- content = " ".join([
227
- item.get("text", "")
228
- for item in content
229
- if item.get("type") == "text"
230
- ])
231
-
232
- # 添加到提示中
233
- if role == "system":
234
- # 系统消息作为 Human 消息的前缀
235
- prompt += f"\n\nHuman: <system>{content}</system>"
236
- elif role == "user":
237
- prompt += f"\n\nHuman: {content}"
238
- elif role == "assistant":
239
- prompt += f"\n\nAssistant: {content}"
240
- # 忽略其他角色
241
-
242
- # 添加最后的 "Assistant:" 提示
243
- prompt += "\n\nAssistant:"
244
- return prompt
245
-
246
-
247
- async def authenticate_client(
248
- auth: Optional[HTTPAuthorizationCredentials] = Depends(security),
249
- ):
250
- """Authenticate client based on API key in Authorization header"""
251
- if not VALID_CLIENT_KEYS:
252
- raise HTTPException(
253
- status_code=503,
254
- detail="Service unavailable: Client API keys not configured on server.",
255
- )
256
-
257
- if not auth or not auth.credentials:
258
- raise HTTPException(
259
- status_code=401,
260
- detail="API key required in Authorization header.",
261
- headers={"WWW-Authenticate": "Bearer"},
262
- )
263
-
264
- if auth.credentials not in VALID_CLIENT_KEYS:
265
- raise HTTPException(status_code=403, detail="Invalid client API key.")
266
-
267
-
268
- @app.on_event("startup")
269
- async def startup():
270
- """应用启动时初始化配置"""
271
- print("Starting Tenbin OpenAI API Adapter server...")
272
- load_client_api_keys()
273
- load_tenbin_accounts()
274
- load_tenbin_models()
275
- print("Server initialization completed.")
276
-
277
-
278
- def get_models_list_response() -> ModelList:
279
- """Helper to construct ModelList response from cached models."""
280
- model_infos = [
281
- ModelInfo(
282
- id=model_id,
283
- created=int(time.time()),
284
- owned_by="tenbin"
285
- )
286
- for model_id in TENBIN_MODELS.keys()
287
- ]
288
- return ModelList(data=model_infos)
289
-
290
-
291
- @app.get("/v1/models", response_model=ModelList)
292
- async def list_v1_models(_: None = Depends(authenticate_client)):
293
- """List available models - authenticated"""
294
- return get_models_list_response()
295
-
296
-
297
- @app.get("/models", response_model=ModelList)
298
- async def list_models_no_auth():
299
- """List available models without authentication - for client compatibility"""
300
- return get_models_list_response()
301
-
302
-
303
- @app.get("/debug")
304
- async def toggle_debug(enable: bool = Query(None)):
305
- """切换调试模式"""
306
- global DEBUG_MODE
307
- if enable is not None:
308
- DEBUG_MODE = enable
309
- return {"debug_mode": DEBUG_MODE}
310
-
311
-
312
- @app.post("/v1/chat/completions")
313
- async def chat_completions(
314
- request: ChatCompletionRequest, _: None = Depends(authenticate_client)
315
- ):
316
- """创建聊天完成 - 使用 Tenbin API"""
317
- # 检查模型是否存在
318
- if request.model not in TENBIN_MODELS:
319
- raise HTTPException(status_code=404, detail=f"Model '{request.model}' not found.")
320
-
321
- # 获取内部模型 ID
322
- internal_model_id = TENBIN_MODELS[request.model]
323
-
324
- if not request.messages:
325
- raise HTTPException(status_code=400, detail="No messages provided in the request.")
326
-
327
- log_debug(f"Processing request for model: {request.model} (internal ID: {internal_model_id})")
328
-
329
- # 构建 Tenbin 格式的提示
330
- prompt = build_tenbin_prompt(request.messages)
331
- log_debug(f"Built prompt with length: {len(prompt)}")
332
-
333
- # 尝试所有账户
334
- for attempt in range(len(TENBIN_ACCOUNTS)):
335
- account = get_best_tenbin_account()
336
- if not account:
337
- raise HTTPException(
338
- status_code=503,
339
- detail="No valid Tenbin accounts available."
340
- )
341
-
342
- session_id = account["session_id"]
343
- log_debug(f"Using account with session_id ending in ...{session_id[-4:]}")
344
-
345
- try:
346
- # 获取执行令牌
347
- execution_token = get_tenbin_execution_token(internal_model_id, session_id)
348
-
349
- if request.stream:
350
- log_debug("Returning stream response")
351
- return StreamingResponse(
352
- tenbin_stream_generator(request.model, prompt, session_id, execution_token),
353
- media_type="text/event-stream",
354
- headers={
355
- "Cache-Control": "no-cache",
356
- "Connection": "keep-alive",
357
- "X-Accel-Buffering": "no",
358
- },
359
- )
360
- else:
361
- log_debug("Building non-stream response")
362
- return build_tenbin_non_stream_response(request.model, prompt, session_id, execution_token)
363
-
364
- except Exception as e:
365
- error_detail = str(e)
366
- log_debug(f"Tenbin API error: {error_detail}")
367
-
368
- with account_rotation_lock:
369
- # 增加错误计数
370
- account["error_count"] += 1
371
- log_debug(f"Account ...{session_id[-4:]} error count: {account['error_count']}")
372
-
373
- # 如果错误看起来是认证问题,标记账户为无效
374
- if "authentication" in error_detail.lower() or "unauthorized" in error_detail.lower():
375
- account["is_valid"] = False
376
- log_debug(f"Account ...{session_id[-4:]} marked as invalid due to auth error.")
377
-
378
- # 所有尝试都失败
379
- if request.stream:
380
- return StreamingResponse(
381
- error_stream_generator("All attempts to contact Tenbin API failed.", 503),
382
- media_type="text/event-stream",
383
- status_code=503,
384
- )
385
- else:
386
- raise HTTPException(status_code=503, detail="All attempts to contact Tenbin API failed.")
387
-
388
-
389
- def get_tenbin_execution_token(model: str, session_id: str) -> str:
390
- """获取 Tenbin 执行令牌"""
391
- try:
392
- task_id = getTaskId()
393
- captcha = getCaptcha(task_id)
394
- url = "https://graphql.tenbin.ai/graphql"
395
-
396
- payload = {
397
- "operationName": "IssueExecutionTokensMultiple",
398
- "variables": {
399
- "turnstileToken": captcha,
400
- "models": [model],
401
- },
402
- "query": "query IssueExecutionTokensMultiple($turnstileToken: String!, $models: [ChatModel!]!) {\n executionTokens: issueExecutionTokensMultiple(\n turnstileToken: $turnstileToken\n models: $models\n )\n}",
403
- }
404
-
405
- headers = {
406
- "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",
407
- "Accept-Encoding": "gzip, deflate, br, zstd",
408
- "Content-Type": "application/json",
409
- "Cookie": f"sessionId={session_id}",
410
- }
411
-
412
- log_debug(f"Getting execution token for model: {model}")
413
- response = requests.post(url, data=json.dumps(payload), headers=headers, timeout=REQUEST_TIMEOUT)
414
- response.raise_for_status()
415
-
416
- execution_token = response.json()["data"]["executionTokens"][0]
417
- log_debug(f"Got execution token: {execution_token[:10]}...")
418
- return execution_token
419
- except Exception as e:
420
- log_debug(f"Error getting execution token: {e}")
421
- raise
422
-
423
-
424
- def tenbin_stream_generator(model: str, prompt: str, session_id: str, execution_token: str):
425
- """Tenbin WebSocket 流式响应生成器"""
426
- stream_id = f"chatcmpl-{uuid.uuid4().hex}"
427
- created_time = int(time.time())
428
-
429
- # 发送初始角色增量
430
- yield f"data: {StreamResponse(id=stream_id, created=created_time, model=model, choices=[StreamChoice(delta={'role': 'assistant'})]).json()}\n\n"
431
-
432
- # 连接 WebSocket
433
- url = "wss://graphql.tenbin.ai/graphql"
434
- headers = {
435
- "Host": "graphql.tenbin.ai",
436
- "Connection": "Upgrade",
437
- "Pragma": "no-cache",
438
- "Cache-Control": "no-cache",
439
- "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",
440
- "Upgrade": "websocket",
441
- "Origin": "https://tenbin.ai",
442
- "Sec-WebSocket-Version": "13",
443
- "Accept-Encoding": "gzip, deflate, br, zstd",
444
- "Accept-Language": "zh-CN,zh;q=0.9",
445
- "Cookie": f"sessionId={session_id}",
446
- "Sec-WebSocket-Key": "I/tTy5psJkboWYQfCypjVA==",
447
- "Sec-WebSocket-Extensions": "permessage-deflate; client_max_window_bits",
448
- "Sec-WebSocket-Protocol": "graphql-transport-ws",
449
- }
450
-
451
- ws = None
452
- try:
453
- log_debug("Connecting to WebSocket...")
454
- ws = websocket.create_connection(url, header=headers)
455
- ws.send(json.dumps({"type": "connection_init"}))
456
- init_response = ws.recv()
457
- log_debug(f"WebSocket init response: {init_response}")
458
-
459
- # 发送订阅请求
460
- payload = {
461
- "id": str(uuid.uuid4()),
462
- "type": "subscribe",
463
- "payload": {
464
- "variables": {
465
- "prompt": prompt,
466
- "executionToken": execution_token,
467
- "stateToken": "",
468
- },
469
- "extensions": {},
470
- "operationName": "StartConversation",
471
- "query": "subscription StartConversation($executionToken: String!, $itemId: String, $itemDraftId: String, $systemPrompt: String, $prompt: String, $stateToken: String, $variables: [ConversationVariableInput!], $itemCallOption: ItemCallOption, $fileKey: String, $fileUploadIds: [String!], $selectedToolsByUser: [ToolType!]) {\n startConversation(\n executionToken: $executionToken\n itemId: $itemId\n itemDraftId: $itemDraftId\n systemPrompt: $systemPrompt\n prompt: $prompt\n stateToken: $stateToken\n variables: $variables\n itemCallOption: $itemCallOption\n fileKey: $fileKey\n fileUploadIds: $fileUploadIds\n selectedToolsByUser: $selectedToolsByUser\n ) {\n ...DeltaConversation\n __typename\n }\n}\n\nfragment DeltaConversation on AIConversationStreamResult {\n seq\n deltaToken\n isFinished\n newStateToken\n error\n fileUploadIds\n toolResult {\n id\n title\n url\n faviconUrl\n summary\n __typename\n }\n action\n activity\n toolError\n __typename\n}",
472
- },
473
- }
474
-
475
- log_debug("Sending subscription request...")
476
- ws.send(json.dumps(payload))
477
-
478
- # 处理响应
479
- accumulated_thinking = ""
480
- thinking_mode = False
481
- thinking_separator = "\n\n---\n\n"
482
- is_thinking_model = model == "Claude-3.7-Sonnet-Extended"
483
-
484
- while True:
485
- try:
486
- msg = ws.recv()
487
- log_debug(f"Received message: {msg[:100]}..." if len(msg) > 100 else msg)
488
-
489
- if msg.endswith('"type":"complete"}'):
490
- log_debug("Received complete message")
491
- break
492
-
493
- try:
494
- data = json.loads(msg)
495
- if data.get("type") != "next":
496
- continue
497
-
498
- payload_data = data.get("payload", {}).get("data", {})
499
- conversation = payload_data.get("startConversation", {})
500
-
501
- delta_token = conversation.get("deltaToken", "")
502
- is_finished = conversation.get("isFinished", False)
503
-
504
- if delta_token:
505
- if is_thinking_model:
506
- # 检查是否包含思考/回答分隔符
507
- if thinking_separator in accumulated_thinking + delta_token:
508
- # 找到分隔符,切换到回答模式
509
- if not thinking_mode:
510
- # 如果之前没有发送过思考内容,先发送累积的思考内容
511
- parts = (accumulated_thinking + delta_token).split(thinking_separator, 1)
512
- thinking_content = parts[0]
513
- answer_content = parts[1] if len(parts) > 1 else ""
514
-
515
- if thinking_content:
516
- yield f"data: {StreamResponse(id=stream_id, created=created_time, model=model, choices=[StreamChoice(delta={'reasoning_content': thinking_content})]).json()}\n\n"
517
-
518
- if answer_content:
519
- yield f"data: {StreamResponse(id=stream_id, created=created_time, model=model, choices=[StreamChoice(delta={'content': answer_content})]).json()}\n\n"
520
-
521
- thinking_mode = True
522
- accumulated_thinking = ""
523
- else:
524
- # 已经在回答模式,直接发送内容
525
- yield f"data: {StreamResponse(id=stream_id, created=created_time, model=model, choices=[StreamChoice(delta={'content': delta_token})]).json()}\n\n"
526
- else:
527
- # 没有找到分隔符
528
- if thinking_mode:
529
- # 已经在回答模式,直接发送内容
530
- yield f"data: {StreamResponse(id=stream_id, created=created_time, model=model, choices=[StreamChoice(delta={'content': delta_token})]).json()}\n\n"
531
- else:
532
- # 继续累积思考内容
533
- accumulated_thinking += delta_token
534
- else:
535
- # 非思考模型,直接发送内容
536
- yield f"data: {StreamResponse(id=stream_id, created=created_time, model=model, choices=[StreamChoice(delta={'content': delta_token})]).json()}\n\n"
537
-
538
- if is_finished:
539
- # 如果还有未发送的思考内容,发送它
540
- if is_thinking_model and not thinking_mode and accumulated_thinking:
541
- yield f"data: {StreamResponse(id=stream_id, created=created_time, model=model, choices=[StreamChoice(delta={'reasoning_content': accumulated_thinking})]).json()}\n\n"
542
-
543
- # 发送完成信号
544
- log_debug("Stream finished")
545
- yield f"data: {StreamResponse(id=stream_id, created=created_time, model=model, choices=[StreamChoice(delta={}, finish_reason='stop')]).json()}\n\n"
546
- yield "data: [DONE]\n\n"
547
- break
548
-
549
- except json.JSONDecodeError as e:
550
- log_debug(f"JSON decode error: {e}")
551
- continue
552
-
553
- except websocket.WebSocketConnectionClosedException:
554
- log_debug("WebSocket connection closed")
555
- break
556
-
557
- except Exception as e:
558
- log_debug(f"Error processing message: {e}")
559
- yield f"data: {json.dumps({'error': str(e)})}\n\n"
560
- break
561
-
562
- except Exception as e:
563
- log_debug(f"WebSocket error: {e}")
564
- yield f"data: {json.dumps({'error': str(e)})}\n\n"
565
- yield "data: [DONE]\n\n"
566
-
567
- finally:
568
- if ws:
569
- try:
570
- ws.close()
571
- log_debug("WebSocket connection closed")
572
- except:
573
- pass
574
-
575
-
576
- def build_tenbin_non_stream_response(model: str, prompt: str, session_id: str, execution_token: str) -> ChatCompletionResponse:
577
- """构建非流式响应"""
578
- full_content = ""
579
- full_reasoning_content = None
580
-
581
- # 使用流式生成器,但累积所有内容
582
- for chunk in tenbin_stream_generator(model, prompt, session_id, execution_token):
583
- if not chunk.startswith("data: ") or chunk.strip() == "data: [DONE]":
584
- continue
585
-
586
- try:
587
- data = json.loads(chunk[6:]) # 去掉 "data: " 前缀
588
- if "choices" not in data:
589
- continue
590
-
591
- delta = data["choices"][0].get("delta", {})
592
-
593
- if "content" in delta and delta["content"]:
594
- full_content += delta["content"]
595
-
596
- if "reasoning_content" in delta and delta["reasoning_content"]:
597
- if full_reasoning_content is None:
598
- full_reasoning_content = ""
599
- full_reasoning_content += delta["reasoning_content"]
600
-
601
- except json.JSONDecodeError:
602
- continue
603
-
604
- return ChatCompletionResponse(
605
- model=model,
606
- choices=[
607
- ChatCompletionChoice(
608
- message=ChatMessage(
609
- role="assistant",
610
- content=full_content,
611
- reasoning_content=full_reasoning_content,
612
- )
613
- )
614
- ],
615
- )
616
-
617
-
618
- async def error_stream_generator(error_detail: str, status_code: int):
619
- """Generate error stream response"""
620
- yield f'data: {json.dumps({"error": {"message": error_detail, "type": "tenbin_api_error", "code": status_code}})}\n\n'
621
- yield "data: [DONE]\n\n"
622
-
623
-
624
- if __name__ == "__main__":
625
- import uvicorn
626
-
627
- # 从环境变量获取端口,默认为 8000
628
- port = int(os.environ.get("PORT", 8000))
629
-
630
- # 设置环境变量以启用调试模式
631
- if os.environ.get("DEBUG_MODE", "").lower() == "true":
632
- DEBUG_MODE = True
633
- print("Debug mode enabled via environment variable")
634
-
635
- if not os.path.exists("tenbin.json") and not os.environ.get("SESSION_IDS"):
636
- print("Warning: tenbin.json not found and SESSION_IDS not set. Creating a dummy file.")
637
- dummy_data = [
638
- {
639
- "session_id": "your_session_id_here",
640
- }
641
- ]
642
- with open("tenbin.json", "w", encoding="utf-8") as f:
643
- json.dump(dummy_data, f, indent=4)
644
- print("Created dummy tenbin.json. Please replace with valid Tenbin data or set SESSION_IDS secret.")
645
-
646
- if not os.path.exists("client_api_keys.json") and not os.environ.get("API_KEYS"):
647
- print("Warning: client_api_keys.json not found and API_KEYS not set. Creating a dummy file.")
648
- dummy_key = f"sk-dummy-{uuid.uuid4().hex}"
649
- with open("client_api_keys.json", "w", encoding="utf-8") as f:
650
- json.dump([dummy_key], f, indent=2)
651
- print(f"Created dummy client_api_keys.json with key: {dummy_key}. Or set API_KEYS secret.")
652
-
653
- if not os.path.exists("models.json"):
654
- print("Warning: models.json not found. Creating a dummy file.")
655
- dummy_models = {
656
- "claude-3.7-sonnet": "AnthropicClaude37Sonnet",
657
- "claude-3.7-sonnet-extended": "AnthropicClaude37SonnetExtended"
658
- }
659
- with open("models.json", "w", encoding="utf-8") as f:
660
- json.dump(dummy_models, f, indent=4)
661
- print("Created dummy models.json.")
662
-
663
- load_client_api_keys()
664
- load_tenbin_accounts()
665
- load_tenbin_models()
666
-
667
- print("\n--- Tenbin OpenAI API Adapter ---")
668
- print(f"Debug Mode: {DEBUG_MODE}")
669
- print("Endpoints:")
670
- print(" GET /v1/models (Client API Key Auth)")
671
- print(" GET /models (No Auth)")
672
- print(" POST /v1/chat/completions (Client API Key Auth)")
673
- print(" GET /debug?enable=[true|false] (Toggle Debug Mode)")
674
-
675
- print(f"\nClient API Keys: {len(VALID_CLIENT_KEYS)}")
676
- if TENBIN_ACCOUNTS:
677
- print(f"Tenbin Accounts: {len(TENBIN_ACCOUNTS)}")
678
- else:
679
- print("Tenbin Accounts: None loaded. Check tenbin.json.")
680
- if TENBIN_MODELS:
681
- models = sorted(list(TENBIN_MODELS.keys()))
682
- print(f"Tenbin Models: {len(TENBIN_MODELS)}")
683
- print(f"Available models: {', '.join(models[:5])}{'...' if len(models) > 5 else ''}")
684
- else:
685
- print("Tenbin Models: None loaded. Check models.json.")
686
- print("------------------------------------")
687
-
 
 
 
 
 
 
 
 
 
 
 
688
  uvicorn.run(app, host="0.0.0.0", port=port)
 
1
+ import json
2
+ import os
3
+ import time
4
+ import uuid
5
+ import threading
6
+ from typing import Any, Dict, List, Optional, TypedDict, Union
7
+
8
+ import requests
9
+ import websocket
10
+ from fastapi import FastAPI, HTTPException, Depends, Query
11
+ from fastapi.responses import StreamingResponse
12
+ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
13
+ from pydantic import BaseModel, Field
14
+
15
+ from getCaptcha import getCaptcha, getTaskId
16
+
17
+
18
+ # Tenbin Account Management
19
+ class TenbinAccount(TypedDict):
20
+ session_id: str
21
+ is_valid: bool
22
+ last_used: float
23
+ error_count: int
24
+
25
+
26
+ # Global variables
27
+ VALID_CLIENT_KEYS: set = set()
28
+ TENBIN_ACCOUNTS: List[TenbinAccount] = []
29
+ TENBIN_MODELS: Dict[str, str] = {} # 模型映射表,key 是模型名称,value 是内部模型 ID
30
+ account_rotation_lock = threading.Lock()
31
+ MAX_ERROR_COUNT = 3
32
+ ERROR_COOLDOWN = 300 # 5 minutes cooldown for accounts with errors
33
+ DEBUG_MODE = os.environ.get("DEBUG_MODE", "false").lower() == "true"
34
+ REQUEST_TIMEOUT = 120.0 # 请求超时时间,秒
35
+ HTTP_PROXY = os.environ.get("HTTP_PROXY")
36
+ PROXIES = {"http": HTTP_PROXY, "https": HTTP_PROXY} if HTTP_PROXY else None
37
+
38
+ if PROXIES:
39
+ print(f"Using HTTP proxy: {HTTP_PROXY}")
40
+
41
+
42
+ # Pydantic Models
43
+ class ChatMessage(BaseModel):
44
+ role: str
45
+ content: Union[str, List[Dict[str, Any]]]
46
+ reasoning_content: Optional[str] = None
47
+
48
+
49
+ class ChatCompletionRequest(BaseModel):
50
+ model: str
51
+ messages: List[ChatMessage]
52
+ stream: bool = True
53
+ temperature: Optional[float] = None
54
+ max_tokens: Optional[int] = None
55
+ top_p: Optional[float] = None
56
+ raw_response: bool = False # ��否返回原始响应
57
+
58
+
59
+ class ModelInfo(BaseModel):
60
+ id: str
61
+ object: str = "model"
62
+ created: int
63
+ owned_by: str = "tenbin"
64
+
65
+
66
+ class ModelList(BaseModel):
67
+ object: str = "list"
68
+ data: List[ModelInfo]
69
+
70
+
71
+ class ChatCompletionChoice(BaseModel):
72
+ message: ChatMessage
73
+ index: int = 0
74
+ finish_reason: str = "stop"
75
+
76
+
77
+ class ChatCompletionResponse(BaseModel):
78
+ id: str = Field(default_factory=lambda: f"chatcmpl-{uuid.uuid4().hex}")
79
+ object: str = "chat.completion"
80
+ created: int = Field(default_factory=lambda: int(time.time()))
81
+ model: str
82
+ choices: List[ChatCompletionChoice]
83
+ usage: Dict[str, int] = Field(
84
+ default_factory=lambda: {
85
+ "prompt_tokens": 0,
86
+ "completion_tokens": 0,
87
+ "total_tokens": 0,
88
+ }
89
+ )
90
+
91
+
92
+ class StreamChoice(BaseModel):
93
+ delta: Dict[str, Any] = Field(default_factory=dict)
94
+ index: int = 0
95
+ finish_reason: Optional[str] = None
96
+
97
+
98
+ class StreamResponse(BaseModel):
99
+ id: str = Field(default_factory=lambda: f"chatcmpl-{uuid.uuid4().hex}")
100
+ object: str = "chat.completion.chunk"
101
+ created: int = Field(default_factory=lambda: int(time.time()))
102
+ model: str
103
+ choices: List[StreamChoice]
104
+
105
+
106
+ # FastAPI App
107
+ app = FastAPI(title="Tenbin OpenAI API Adapter")
108
+ security = HTTPBearer(auto_error=False)
109
+
110
+
111
+ def log_debug(message: str):
112
+ """Debug日志函数"""
113
+ if DEBUG_MODE:
114
+ print(f"[DEBUG] {message}")
115
+
116
+
117
+ def load_client_api_keys():
118
+ """Load client API keys from environment variable (comma-separated) or client_api_keys.json"""
119
+ global VALID_CLIENT_KEYS
120
+ keys_str = os.environ.get("API_KEYS")
121
+ try:
122
+ if keys_str:
123
+ print("Loading client API keys from API_KEYS environment variable.")
124
+ keys = [key.strip() for key in keys_str.split(',')]
125
+ else:
126
+ print("Loading client API keys from file: client_api_keys.json")
127
+ with open("client_api_keys.json", "r", encoding="utf-8") as f:
128
+ keys = json.load(f)
129
+
130
+ VALID_CLIENT_KEYS = set(keys) if isinstance(keys, list) else set()
131
+ print(f"Successfully loaded {len(VALID_CLIENT_KEYS)} client API keys.")
132
+ except FileNotFoundError:
133
+ print("Error: client_api_keys.json not found and API_KEYS not set. Client authentication will fail.")
134
+ VALID_CLIENT_KEYS = set()
135
+ except Exception as e:
136
+ print(f"Error loading client API keys: {e}")
137
+ VALID_CLIENT_KEYS = set()
138
+
139
+
140
+ def load_tenbin_accounts():
141
+ """Load Tenbin accounts from environment variable (comma-separated session_ids) or tenbin.json"""
142
+ global TENBIN_ACCOUNTS
143
+ TENBIN_ACCOUNTS = []
144
+ session_ids_str = os.environ.get("SESSION_IDS")
145
+ try:
146
+ accounts_to_process = []
147
+ if session_ids_str:
148
+ print("Loading Tenbin accounts from SESSION_IDS environment variable.")
149
+ session_ids = [sid.strip() for sid in session_ids_str.split(',')]
150
+ accounts_to_process = [{"session_id": sid} for sid in session_ids]
151
+ else:
152
+ print("Loading Tenbin accounts from file: tenbin.json")
153
+ with open("tenbin.json", "r", encoding="utf-8") as f:
154
+ accounts_to_process = json.load(f)
155
+
156
+ if not isinstance(accounts_to_process, list):
157
+ print("Warning: Account data should be a list of objects.")
158
+ return
159
+
160
+ for acc in accounts_to_process:
161
+ session_id = acc.get("session_id")
162
+ if session_id:
163
+ TENBIN_ACCOUNTS.append({
164
+ "session_id": session_id,
165
+ "is_valid": True,
166
+ "last_used": 0,
167
+ "error_count": 0
168
+ })
169
+ print(f"Successfully loaded {len(TENBIN_ACCOUNTS)} Tenbin accounts.")
170
+ except FileNotFoundError:
171
+ print("Error: tenbin.json not found and SESSION_IDS not set. API calls will fail.")
172
+ except Exception as e:
173
+ print(f"Error loading tenbin.json: {e}")
174
+
175
+
176
+ def load_tenbin_models():
177
+ """Load Tenbin models from models.json"""
178
+ global TENBIN_MODELS
179
+ try:
180
+ with open("models.json", "r", encoding="utf-8") as f:
181
+ models_data = json.load(f)
182
+ if isinstance(models_data, dict):
183
+ TENBIN_MODELS = models_data
184
+ print(f"Successfully loaded {len(TENBIN_MODELS)} models.")
185
+ else:
186
+ print("Warning: models.json should contain a dictionary of model mappings.")
187
+ TENBIN_MODELS = {}
188
+ except FileNotFoundError:
189
+ print("Error: models.json not found. Model list will be empty.")
190
+ TENBIN_MODELS = {}
191
+ except Exception as e:
192
+ print(f"Error loading models.json: {e}")
193
+ TENBIN_MODELS = {}
194
+
195
+
196
+ def get_best_tenbin_account() -> Optional[TenbinAccount]:
197
+ """Get the best available Tenbin account using a smart selection algorithm."""
198
+ with account_rotation_lock:
199
+ now = time.time()
200
+ valid_accounts = [
201
+ acc for acc in TENBIN_ACCOUNTS
202
+ if acc["is_valid"] and (
203
+ acc["error_count"] < MAX_ERROR_COUNT or
204
+ now - acc["last_used"] > ERROR_COOLDOWN
205
+ )
206
+ ]
207
+
208
+ if not valid_accounts:
209
+ return None
210
+
211
+ # Reset error count for accounts that have been in cooldown
212
+ for acc in valid_accounts:
213
+ if acc["error_count"] >= MAX_ERROR_COUNT and now - acc["last_used"] > ERROR_COOLDOWN:
214
+ acc["error_count"] = 0
215
+
216
+ # Sort by last used (oldest first) and error count (lowest first)
217
+ valid_accounts.sort(key=lambda x: (x["last_used"], x["error_count"]))
218
+ account = valid_accounts[0]
219
+ account["last_used"] = now
220
+ return account
221
+
222
+
223
+ def build_tenbin_prompt(messages: List[ChatMessage]) -> str:
224
+ """将 OpenAI 格式的消息列表转换为 Tenbin 格式的单个字符串"""
225
+ prompt = ""
226
+ for msg in messages:
227
+ role = msg.role
228
+ content = msg.content
229
+ if isinstance(content, list):
230
+ # 简单处理多模态内容,只提取文本部分
231
+ content = " ".join([
232
+ item.get("text", "")
233
+ for item in content
234
+ if item.get("type") == "text"
235
+ ])
236
+
237
+ # 添加到提示中
238
+ if role == "system":
239
+ # 系统消息作为 Human 消息的前缀
240
+ prompt += f"\n\nHuman: <system>{content}</system>"
241
+ elif role == "user":
242
+ prompt += f"\n\nHuman: {content}"
243
+ elif role == "assistant":
244
+ prompt += f"\n\nAssistant: {content}"
245
+ # 忽略其他角色
246
+
247
+ # 添加最后的 "Assistant:" 提示
248
+ prompt += "\n\nAssistant:"
249
+ return prompt
250
+
251
+
252
+ async def authenticate_client(
253
+ auth: Optional[HTTPAuthorizationCredentials] = Depends(security),
254
+ ):
255
+ """Authenticate client based on API key in Authorization header"""
256
+ if not VALID_CLIENT_KEYS:
257
+ raise HTTPException(
258
+ status_code=503,
259
+ detail="Service unavailable: Client API keys not configured on server.",
260
+ )
261
+
262
+ if not auth or not auth.credentials:
263
+ raise HTTPException(
264
+ status_code=401,
265
+ detail="API key required in Authorization header.",
266
+ headers={"WWW-Authenticate": "Bearer"},
267
+ )
268
+
269
+ if auth.credentials not in VALID_CLIENT_KEYS:
270
+ raise HTTPException(status_code=403, detail="Invalid client API key.")
271
+
272
+
273
+ @app.on_event("startup")
274
+ async def startup():
275
+ """应用启动时初始化配置"""
276
+ print("Starting Tenbin OpenAI API Adapter server...")
277
+ load_client_api_keys()
278
+ load_tenbin_accounts()
279
+ load_tenbin_models()
280
+ print("Server initialization completed.")
281
+
282
+
283
+ def get_models_list_response() -> ModelList:
284
+ """Helper to construct ModelList response from cached models."""
285
+ model_infos = [
286
+ ModelInfo(
287
+ id=model_id,
288
+ created=int(time.time()),
289
+ owned_by="tenbin"
290
+ )
291
+ for model_id in TENBIN_MODELS.keys()
292
+ ]
293
+ return ModelList(data=model_infos)
294
+
295
+
296
+ @app.get("/v1/models", response_model=ModelList)
297
+ async def list_v1_models(_: None = Depends(authenticate_client)):
298
+ """List available models - authenticated"""
299
+ return get_models_list_response()
300
+
301
+
302
+ @app.get("/models", response_model=ModelList)
303
+ async def list_models_no_auth():
304
+ """List available models without authentication - for client compatibility"""
305
+ return get_models_list_response()
306
+
307
+
308
+ @app.get("/debug")
309
+ async def toggle_debug(enable: bool = Query(None)):
310
+ """切换调试模式"""
311
+ global DEBUG_MODE
312
+ if enable is not None:
313
+ DEBUG_MODE = enable
314
+ return {"debug_mode": DEBUG_MODE}
315
+
316
+
317
+ @app.post("/v1/chat/completions")
318
+ async def chat_completions(
319
+ request: ChatCompletionRequest, _: None = Depends(authenticate_client)
320
+ ):
321
+ """创建聊天完成 - 使用 Tenbin API"""
322
+ # 检查模型是否存在
323
+ if request.model not in TENBIN_MODELS:
324
+ raise HTTPException(status_code=404, detail=f"Model '{request.model}' not found.")
325
+
326
+ # 获取内部模型 ID
327
+ internal_model_id = TENBIN_MODELS[request.model]
328
+
329
+ if not request.messages:
330
+ raise HTTPException(status_code=400, detail="No messages provided in the request.")
331
+
332
+ log_debug(f"Processing request for model: {request.model} (internal ID: {internal_model_id})")
333
+
334
+ # 构建 Tenbin 格式的提示
335
+ prompt = build_tenbin_prompt(request.messages)
336
+ log_debug(f"Built prompt with length: {len(prompt)}")
337
+
338
+ # 尝试所有账户
339
+ for attempt in range(len(TENBIN_ACCOUNTS)):
340
+ account = get_best_tenbin_account()
341
+ if not account:
342
+ raise HTTPException(
343
+ status_code=503,
344
+ detail="No valid Tenbin accounts available."
345
+ )
346
+
347
+ session_id = account["session_id"]
348
+ log_debug(f"Using account with session_id ending in ...{session_id[-4:]}")
349
+
350
+ try:
351
+ # 获��执行令牌
352
+ execution_token = get_tenbin_execution_token(internal_model_id, session_id)
353
+
354
+ if request.stream:
355
+ log_debug("Returning stream response")
356
+ return StreamingResponse(
357
+ tenbin_stream_generator(request.model, prompt, session_id, execution_token),
358
+ media_type="text/event-stream",
359
+ headers={
360
+ "Cache-Control": "no-cache",
361
+ "Connection": "keep-alive",
362
+ "X-Accel-Buffering": "no",
363
+ },
364
+ )
365
+ else:
366
+ log_debug("Building non-stream response")
367
+ return build_tenbin_non_stream_response(request.model, prompt, session_id, execution_token)
368
+
369
+ except Exception as e:
370
+ error_detail = str(e)
371
+ log_debug(f"Tenbin API error: {error_detail}")
372
+
373
+ with account_rotation_lock:
374
+ # 增加错误计数
375
+ account["error_count"] += 1
376
+ log_debug(f"Account ...{session_id[-4:]} error count: {account['error_count']}")
377
+
378
+ # 如果错误看起来是认证问题,标记账户为无效
379
+ if "authentication" in error_detail.lower() or "unauthorized" in error_detail.lower():
380
+ account["is_valid"] = False
381
+ log_debug(f"Account ...{session_id[-4:]} marked as invalid due to auth error.")
382
+
383
+ # 所有尝试都失败
384
+ if request.stream:
385
+ return StreamingResponse(
386
+ error_stream_generator("All attempts to contact Tenbin API failed.", 503),
387
+ media_type="text/event-stream",
388
+ status_code=503,
389
+ )
390
+ else:
391
+ raise HTTPException(status_code=503, detail="All attempts to contact Tenbin API failed.")
392
+
393
+
394
+ def get_tenbin_execution_token(model: str, session_id: str) -> str:
395
+ """获取 Tenbin 执行令牌"""
396
+ try:
397
+ task_id = getTaskId()
398
+ captcha = getCaptcha(task_id)
399
+ url = "https://graphql.tenbin.ai/graphql"
400
+
401
+ payload = {
402
+ "operationName": "IssueExecutionTokensMultiple",
403
+ "variables": {
404
+ "turnstileToken": captcha,
405
+ "models": [model],
406
+ },
407
+ "query": "query IssueExecutionTokensMultiple($turnstileToken: String!, $models: [ChatModel!]!) {\n executionTokens: issueExecutionTokensMultiple(\n turnstileToken: $turnstileToken\n models: $models\n )\n}",
408
+ }
409
+
410
+ headers = {
411
+ "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",
412
+ "Accept-Encoding": "gzip, deflate, br, zstd",
413
+ "Content-Type": "application/json",
414
+ "Cookie": f"sessionId={session_id}",
415
+ }
416
+
417
+ log_debug(f"Getting execution token for model: {model}")
418
+ response = requests.post(
419
+ url,
420
+ data=json.dumps(payload),
421
+ headers=headers,
422
+ timeout=REQUEST_TIMEOUT,
423
+ proxies=PROXIES
424
+ )
425
+ response.raise_for_status()
426
+
427
+ execution_token = response.json()["data"]["executionTokens"][0]
428
+ log_debug(f"Got execution token: {execution_token[:10]}...")
429
+ return execution_token
430
+ except Exception as e:
431
+ log_debug(f"Error getting execution token: {e}")
432
+ raise
433
+
434
+
435
+ def tenbin_stream_generator(model: str, prompt: str, session_id: str, execution_token: str):
436
+ """Tenbin WebSocket 流式响应生成器"""
437
+ stream_id = f"chatcmpl-{uuid.uuid4().hex}"
438
+ created_time = int(time.time())
439
+
440
+ # 发送初始角色增量
441
+ yield f"data: {StreamResponse(id=stream_id, created=created_time, model=model, choices=[StreamChoice(delta={'role': 'assistant'})]).json()}\n\n"
442
+
443
+ # 连接 WebSocket
444
+ url = "wss://graphql.tenbin.ai/graphql"
445
+ headers = {
446
+ "Host": "graphql.tenbin.ai",
447
+ "Connection": "Upgrade",
448
+ "Pragma": "no-cache",
449
+ "Cache-Control": "no-cache",
450
+ "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",
451
+ "Upgrade": "websocket",
452
+ "Origin": "https://tenbin.ai",
453
+ "Sec-WebSocket-Version": "13",
454
+ "Accept-Encoding": "gzip, deflate, br, zstd",
455
+ "Accept-Language": "zh-CN,zh;q=0.9",
456
+ "Cookie": f"sessionId={session_id}",
457
+ "Sec-WebSocket-Key": "I/tTy5psJkboWYQfCypjVA==",
458
+ "Sec-WebSocket-Extensions": "permessage-deflate; client_max_window_bits",
459
+ "Sec-WebSocket-Protocol": "graphql-transport-ws",
460
+ }
461
+
462
+ ws = None
463
+ try:
464
+ log_debug("Connecting to WebSocket...")
465
+ ws = websocket.create_connection(url, header=headers)
466
+ ws.send(json.dumps({"type": "connection_init"}))
467
+ init_response = ws.recv()
468
+ log_debug(f"WebSocket init response: {init_response}")
469
+
470
+ # 发送订���请求
471
+ payload = {
472
+ "id": str(uuid.uuid4()),
473
+ "type": "subscribe",
474
+ "payload": {
475
+ "variables": {
476
+ "prompt": prompt,
477
+ "executionToken": execution_token,
478
+ "stateToken": "",
479
+ },
480
+ "extensions": {},
481
+ "operationName": "StartConversation",
482
+ "query": "subscription StartConversation($executionToken: String!, $itemId: String, $itemDraftId: String, $systemPrompt: String, $prompt: String, $stateToken: String, $variables: [ConversationVariableInput!], $itemCallOption: ItemCallOption, $fileKey: String, $fileUploadIds: [String!], $selectedToolsByUser: [ToolType!]) {\n startConversation(\n executionToken: $executionToken\n itemId: $itemId\n itemDraftId: $itemDraftId\n systemPrompt: $systemPrompt\n prompt: $prompt\n stateToken: $stateToken\n variables: $variables\n itemCallOption: $itemCallOption\n fileKey: $fileKey\n fileUploadIds: $fileUploadIds\n selectedToolsByUser: $selectedToolsByUser\n ) {\n ...DeltaConversation\n __typename\n }\n}\n\nfragment DeltaConversation on AIConversationStreamResult {\n seq\n deltaToken\n isFinished\n newStateToken\n error\n fileUploadIds\n toolResult {\n id\n title\n url\n faviconUrl\n summary\n __typename\n }\n action\n activity\n toolError\n __typename\n}",
483
+ },
484
+ }
485
+
486
+ log_debug("Sending subscription request...")
487
+ ws.send(json.dumps(payload))
488
+
489
+ # 处理响应
490
+ accumulated_thinking = ""
491
+ thinking_mode = False
492
+ thinking_separator = "\n\n---\n\n"
493
+ is_thinking_model = model == "Claude-3.7-Sonnet-Extended"
494
+
495
+ while True:
496
+ try:
497
+ msg = ws.recv()
498
+ log_debug(f"Received message: {msg[:100]}..." if len(msg) > 100 else msg)
499
+
500
+ if msg.endswith('"type":"complete"}'):
501
+ log_debug("Received complete message")
502
+ break
503
+
504
+ try:
505
+ data = json.loads(msg)
506
+ if data.get("type") != "next":
507
+ continue
508
+
509
+ payload_data = data.get("payload", {}).get("data", {})
510
+ conversation = payload_data.get("startConversation", {})
511
+
512
+ delta_token = conversation.get("deltaToken", "")
513
+ is_finished = conversation.get("isFinished", False)
514
+
515
+ if delta_token:
516
+ if is_thinking_model:
517
+ # 检查是否包含思考/回答分隔符
518
+ if thinking_separator in accumulated_thinking + delta_token:
519
+ # 找到分隔符,切换到回答模式
520
+ if not thinking_mode:
521
+ # 如果之前没有发送过思考内容,先发送累积的思考内容
522
+ parts = (accumulated_thinking + delta_token).split(thinking_separator, 1)
523
+ thinking_content = parts[0]
524
+ answer_content = parts[1] if len(parts) > 1 else ""
525
+
526
+ if thinking_content:
527
+ yield f"data: {StreamResponse(id=stream_id, created=created_time, model=model, choices=[StreamChoice(delta={'reasoning_content': thinking_content})]).json()}\n\n"
528
+
529
+ if answer_content:
530
+ yield f"data: {StreamResponse(id=stream_id, created=created_time, model=model, choices=[StreamChoice(delta={'content': answer_content})]).json()}\n\n"
531
+
532
+ thinking_mode = True
533
+ accumulated_thinking = ""
534
+ else:
535
+ # 已经在回答模式,直接发送内容
536
+ yield f"data: {StreamResponse(id=stream_id, created=created_time, model=model, choices=[StreamChoice(delta={'content': delta_token})]).json()}\n\n"
537
+ else:
538
+ # 没有找到分隔符
539
+ if thinking_mode:
540
+ # 已经在回答模式,直接发送内容
541
+ yield f"data: {StreamResponse(id=stream_id, created=created_time, model=model, choices=[StreamChoice(delta={'content': delta_token})]).json()}\n\n"
542
+ else:
543
+ # 继续累积思考内容
544
+ accumulated_thinking += delta_token
545
+ else:
546
+ # 非思��模型,直接发送内容
547
+ yield f"data: {StreamResponse(id=stream_id, created=created_time, model=model, choices=[StreamChoice(delta={'content': delta_token})]).json()}\n\n"
548
+
549
+ if is_finished:
550
+ # 如果还有未发送的思考内容,发送它
551
+ if is_thinking_model and not thinking_mode and accumulated_thinking:
552
+ yield f"data: {StreamResponse(id=stream_id, created=created_time, model=model, choices=[StreamChoice(delta={'reasoning_content': accumulated_thinking})]).json()}\n\n"
553
+
554
+ # 发送完成信号
555
+ log_debug("Stream finished")
556
+ yield f"data: {StreamResponse(id=stream_id, created=created_time, model=model, choices=[StreamChoice(delta={}, finish_reason='stop')]).json()}\n\n"
557
+ yield "data: [DONE]\n\n"
558
+ break
559
+
560
+ except json.JSONDecodeError as e:
561
+ log_debug(f"JSON decode error: {e}")
562
+ continue
563
+
564
+ except websocket.WebSocketConnectionClosedException:
565
+ log_debug("WebSocket connection closed")
566
+ break
567
+
568
+ except Exception as e:
569
+ log_debug(f"Error processing message: {e}")
570
+ yield f"data: {json.dumps({'error': str(e)})}\n\n"
571
+ break
572
+
573
+ except Exception as e:
574
+ log_debug(f"WebSocket error: {e}")
575
+ yield f"data: {json.dumps({'error': str(e)})}\n\n"
576
+ yield "data: [DONE]\n\n"
577
+
578
+ finally:
579
+ if ws:
580
+ try:
581
+ ws.close()
582
+ log_debug("WebSocket connection closed")
583
+ except:
584
+ pass
585
+
586
+
587
+ def build_tenbin_non_stream_response(model: str, prompt: str, session_id: str, execution_token: str) -> ChatCompletionResponse:
588
+ """构建非流式响应"""
589
+ full_content = ""
590
+ full_reasoning_content = None
591
+
592
+ # 使用流式生成器,但累积所有内容
593
+ for chunk in tenbin_stream_generator(model, prompt, session_id, execution_token):
594
+ if not chunk.startswith("data: ") or chunk.strip() == "data: [DONE]":
595
+ continue
596
+
597
+ try:
598
+ data = json.loads(chunk[6:]) # 去掉 "data: " 前缀
599
+ if "choices" not in data:
600
+ continue
601
+
602
+ delta = data["choices"][0].get("delta", {})
603
+
604
+ if "content" in delta and delta["content"]:
605
+ full_content += delta["content"]
606
+
607
+ if "reasoning_content" in delta and delta["reasoning_content"]:
608
+ if full_reasoning_content is None:
609
+ full_reasoning_content = ""
610
+ full_reasoning_content += delta["reasoning_content"]
611
+
612
+ except json.JSONDecodeError:
613
+ continue
614
+
615
+ return ChatCompletionResponse(
616
+ model=model,
617
+ choices=[
618
+ ChatCompletionChoice(
619
+ message=ChatMessage(
620
+ role="assistant",
621
+ content=full_content,
622
+ reasoning_content=full_reasoning_content,
623
+ )
624
+ )
625
+ ],
626
+ )
627
+
628
+
629
+ async def error_stream_generator(error_detail: str, status_code: int):
630
+ """Generate error stream response"""
631
+ yield f'data: {json.dumps({"error": {"message": error_detail, "type": "tenbin_api_error", "code": status_code}})}\n\n'
632
+ yield "data: [DONE]\n\n"
633
+
634
+
635
+ if __name__ == "__main__":
636
+ import uvicorn
637
+
638
+ # 从环境变量获取端口,默认为 8000
639
+ port = int(os.environ.get("PORT", 8000))
640
+
641
+ # 设置环境变量以启用调试模式
642
+ if os.environ.get("DEBUG_MODE", "").lower() == "true":
643
+ DEBUG_MODE = True
644
+ print("Debug mode enabled via environment variable")
645
+
646
+ if not os.path.exists("tenbin.json") and not os.environ.get("SESSION_IDS"):
647
+ print("Warning: tenbin.json not found and SESSION_IDS not set. Creating a dummy file.")
648
+ dummy_data = [
649
+ {
650
+ "session_id": "your_session_id_here",
651
+ }
652
+ ]
653
+ with open("tenbin.json", "w", encoding="utf-8") as f:
654
+ json.dump(dummy_data, f, indent=4)
655
+ print("Created dummy tenbin.json. Please replace with valid Tenbin data or set SESSION_IDS secret.")
656
+
657
+ if not os.path.exists("client_api_keys.json") and not os.environ.get("API_KEYS"):
658
+ print("Warning: client_api_keys.json not found and API_KEYS not set. Creating a dummy file.")
659
+ dummy_key = f"sk-dummy-{uuid.uuid4().hex}"
660
+ with open("client_api_keys.json", "w", encoding="utf-8") as f:
661
+ json.dump([dummy_key], f, indent=2)
662
+ print(f"Created dummy client_api_keys.json with key: {dummy_key}. Or set API_KEYS secret.")
663
+
664
+ if not os.path.exists("models.json"):
665
+ print("Warning: models.json not found. Creating a dummy file.")
666
+ dummy_models = {
667
+ "claude-3.7-sonnet": "AnthropicClaude37Sonnet",
668
+ "claude-3.7-sonnet-extended": "AnthropicClaude37SonnetExtended"
669
+ }
670
+ with open("models.json", "w", encoding="utf-8") as f:
671
+ json.dump(dummy_models, f, indent=4)
672
+ print("Created dummy models.json.")
673
+
674
+ load_client_api_keys()
675
+ load_tenbin_accounts()
676
+ load_tenbin_models()
677
+
678
+ print("\n--- Tenbin OpenAI API Adapter ---")
679
+ print(f"Debug Mode: {DEBUG_MODE}")
680
+ print("Endpoints:")
681
+ print(" GET /v1/models (Client API Key Auth)")
682
+ print(" GET /models (No Auth)")
683
+ print(" POST /v1/chat/completions (Client API Key Auth)")
684
+ print(" GET /debug?enable=[true|false] (Toggle Debug Mode)")
685
+
686
+ print(f"\nClient API Keys: {len(VALID_CLIENT_KEYS)}")
687
+ if TENBIN_ACCOUNTS:
688
+ print(f"Tenbin Accounts: {len(TENBIN_ACCOUNTS)}")
689
+ else:
690
+ print("Tenbin Accounts: None loaded. Check tenbin.json.")
691
+ if TENBIN_MODELS:
692
+ models = sorted(list(TENBIN_MODELS.keys()))
693
+ print(f"Tenbin Models: {len(TENBIN_MODELS)}")
694
+ print(f"Available models: {', '.join(models[:5])}{'...' if len(models) > 5 else ''}")
695
+ else:
696
+ print("Tenbin Models: None loaded. Check models.json.")
697
+ print("------------------------------------")
698
+
699
  uvicorn.run(app, host="0.0.0.0", port=port)
resigner.py CHANGED
@@ -1,135 +1,141 @@
1
- import time
2
- import requests
3
- import uuid
4
- import random
5
- import string
6
-
7
- TURNSTILE_URL = "http://" # 自行搭建的turnstile solver服务
8
- API_KEY = "" # mail.eleme.uk的api
9
-
10
- def create_task():
11
- url = f"{TURNSTILE_URL}/turnstile"
12
- resp = requests.get(url, params={
13
- "url": "https://oshiete.ai/email_confirmation",
14
- "sitekey": "0x4AAAAAABGR2exxRproizri",
15
- "action": "request_registration_link"
16
- })
17
- print(resp.json())
18
- return resp.json()['task_id']
19
-
20
-
21
- def get_result(task_id):
22
- url = f"{TURNSTILE_URL}/result"
23
- resp = requests.get(url, params={
24
- "id": task_id
25
- })
26
- if 'value' in resp.text:
27
- return resp.json()['value']
28
- return None
29
-
30
- def solve_turnstile():
31
- task_id = create_task()
32
- while True:
33
- result = get_result(task_id)
34
- if result:
35
- return result
36
- break
37
- time.sleep(1)
38
-
39
- def generate_random_string(length=10):
40
- characters = string.ascii_letters + string.digits
41
- random_string = ''.join(random.choice(characters) for _ in range(length))
42
- return random_string
43
-
44
-
45
- def get_email():
46
- url = "https://mail.eleme.uk/api/emails/generate"
47
- headers = {
48
- "X-API-Key": API_KEY,
49
- "Content-Type": "application/json"
50
- }
51
- data = {
52
- "name": generate_random_string(8),
53
- "expiryTime": 3600000,
54
- "domain": "ele.edu.kg"
55
- }
56
-
57
- try:
58
- response = requests.post(url, headers=headers, json=data)
59
- response.raise_for_status() # 检查响应状态
60
- email_data = response.json()
61
- return email_data.get("email"), email_data.get("id") # 假设API返回包含email字段的JSON
62
- except requests.exceptions.RequestException as e:
63
- print(f"获取邮箱时出错: {e}")
64
- return None
65
-
66
- def get_code(id: str):
67
- url = f"https://mail.eleme.uk/api/emails/{id}"
68
- headers = {
69
- "X-API-Key": API_KEY
70
- }
71
-
72
- try:
73
- response = requests.get(url, headers=headers)
74
- response.raise_for_status()
75
- emails_data = response.json()
76
- # 提取验证码逻辑可能需要根据实际邮件内容调整
77
- for email in emails_data['messages']:
78
- if 'yGMO' in email['subject']:
79
- # 获取邮件内容的详细信息
80
- message_id = email['id']
81
- url_message = f"https://mail.eleme.uk/api/emails/{id}/{message_id}"
82
- message_response = requests.get(url_message, headers=headers)
83
- message_response.raise_for_status()
84
- message_data = message_response.json()
85
- return message_data['message']['html'].split('code=')[1].split('<br>')[0]
86
- except requests.exceptions.RequestException as e:
87
- print(f"获取验证码时出错: {e}")
88
- return None
89
-
90
- def send_email(email: str):
91
- url = "https://graphql.oshiete.ai/graphql"
92
- resp = requests.post(url, json={
93
- "operationName": "RequestRegistrationLink",
94
- "variables": {
95
- "email": email,
96
- "turnstileToken": solve_turnstile()
97
- },
98
- "query": """
99
- mutation RequestRegistrationLink($email: String!, $turnstileToken: String!) {
100
- requestRegistrationLink(email: $email, turnstileToken: $turnstileToken)
101
- }
102
- """
103
- })
104
-
105
- def register(code: str):
106
- url = "https://graphql.oshiete.ai/graphql"
107
- resp = requests.post(url, json={
108
- "operationName": "RegisterUser",
109
- "variables": {
110
- "code": code,
111
- "dti": str(uuid.uuid4()),
112
- "password": "Aa123321."
113
- },
114
- "query": """
115
- mutation RegisterUser($code: String!, $password: String!, $dti: String) {
116
- registerUser(code: $code, password: $password, dti: $dti) {
117
- id
118
- __typename
119
- }
120
- }
121
- """
122
- })
123
- # 提取sessionId cookie
124
- session_id = resp.cookies.get('sessionId')
125
- return session_id
126
-
127
- if __name__ == "__main__":
128
- email, id = get_email()
129
- send_email(email)
130
- code = None
131
- while not code:
132
- code = get_code(id)
133
- time.sleep(1)
134
- session_id = register(code)
 
 
 
 
 
 
135
  print(session_id)
 
1
+ import time
2
+ import requests
3
+ import uuid
4
+ import random
5
+ import string
6
+ import os
7
+
8
+ TURNSTILE_URL = "http://" # 自行搭建的turnstile solver服务
9
+ API_KEY = "" # mail.eleme.uk的api
10
+ HTTP_PROXY = os.environ.get("HTTP_PROXY")
11
+ PROXIES = {"http": HTTP_PROXY, "https": HTTP_PROXY} if HTTP_PROXY else None
12
+
13
+ if PROXIES:
14
+ print(f"Using HTTP proxy: {HTTP_PROXY}")
15
+
16
+ def create_task():
17
+ url = f"{TURNSTILE_URL}/turnstile"
18
+ resp = requests.get(url, params={
19
+ "url": "https://oshiete.ai/email_confirmation",
20
+ "sitekey": "0x4AAAAAABGR2exxRproizri",
21
+ "action": "request_registration_link"
22
+ })
23
+ print(resp.json())
24
+ return resp.json()['task_id']
25
+
26
+
27
+ def get_result(task_id):
28
+ url = f"{TURNSTILE_URL}/result"
29
+ resp = requests.get(url, params={
30
+ "id": task_id
31
+ })
32
+ if 'value' in resp.text:
33
+ return resp.json()['value']
34
+ return None
35
+
36
+ def solve_turnstile():
37
+ task_id = create_task()
38
+ while True:
39
+ result = get_result(task_id)
40
+ if result:
41
+ return result
42
+ break
43
+ time.sleep(1)
44
+
45
+ def generate_random_string(length=10):
46
+ characters = string.ascii_letters + string.digits
47
+ random_string = ''.join(random.choice(characters) for _ in range(length))
48
+ return random_string
49
+
50
+
51
+ def get_email():
52
+ url = "https://mail.eleme.uk/api/emails/generate"
53
+ headers = {
54
+ "X-API-Key": API_KEY,
55
+ "Content-Type": "application/json"
56
+ }
57
+ data = {
58
+ "name": generate_random_string(8),
59
+ "expiryTime": 3600000,
60
+ "domain": "ele.edu.kg"
61
+ }
62
+
63
+ try:
64
+ response = requests.post(url, headers=headers, json=data, proxies=PROXIES)
65
+ response.raise_for_status() # 检查响应状态
66
+ email_data = response.json()
67
+ return email_data.get("email"), email_data.get("id") # 假设API返回包含email字段的JSON
68
+ except requests.exceptions.RequestException as e:
69
+ print(f"获取邮箱时出错: {e}")
70
+ return None
71
+
72
+ def get_code(id: str):
73
+ url = f"https://mail.eleme.uk/api/emails/{id}"
74
+ headers = {
75
+ "X-API-Key": API_KEY
76
+ }
77
+
78
+ try:
79
+ response = requests.get(url, headers=headers, proxies=PROXIES)
80
+ response.raise_for_status()
81
+ emails_data = response.json()
82
+ # 提取验证码逻辑可能需要根据实际邮件内容调整
83
+ for email in emails_data['messages']:
84
+ if 'yGMO' in email['subject']:
85
+ # 获取邮件内容的详细信息
86
+ message_id = email['id']
87
+ url_message = f"https://mail.eleme.uk/api/emails/{id}/{message_id}"
88
+ message_response = requests.get(url_message, headers=headers, proxies=PROXIES)
89
+ message_response.raise_for_status()
90
+ message_data = message_response.json()
91
+ return message_data['message']['html'].split('code=')[1].split('<br>')[0]
92
+ except requests.exceptions.RequestException as e:
93
+ print(f"获取验证码时出错: {e}")
94
+ return None
95
+
96
+ def send_email(email: str):
97
+ url = "https://graphql.oshiete.ai/graphql"
98
+ resp = requests.post(url, json={
99
+ "operationName": "RequestRegistrationLink",
100
+ "variables": {
101
+ "email": email,
102
+ "turnstileToken": solve_turnstile()
103
+ },
104
+ "query": """
105
+ mutation RequestRegistrationLink($email: String!, $turnstileToken: String!) {
106
+ requestRegistrationLink(email: $email, turnstileToken: $turnstileToken)
107
+ }
108
+ """
109
+ }, proxies=PROXIES)
110
+
111
+ def register(code: str):
112
+ url = "https://graphql.oshiete.ai/graphql"
113
+ resp = requests.post(url, json={
114
+ "operationName": "RegisterUser",
115
+ "variables": {
116
+ "code": code,
117
+ "dti": str(uuid.uuid4()),
118
+ "password": "Aa123321."
119
+ },
120
+ "query": """
121
+ mutation RegisterUser($code: String!, $password: String!, $dti: String) {
122
+ registerUser(code: $code, password: $password, dti: $dti) {
123
+ id
124
+ __typename
125
+ }
126
+ }
127
+ """
128
+ }, proxies=PROXIES)
129
+ # 提取sessionId cookie
130
+ session_id = resp.cookies.get('sessionId')
131
+ return session_id
132
+
133
+ if __name__ == "__main__":
134
+ email, id = get_email()
135
+ send_email(email)
136
+ code = None
137
+ while not code:
138
+ code = get_code(id)
139
+ time.sleep(1)
140
+ session_id = register(code)
141
  print(session_id)