yxmiler commited on
Commit
23be89c
·
verified ·
1 Parent(s): 8c2c8b3

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +756 -365
app.py CHANGED
@@ -1,49 +1,86 @@
1
  import os
2
  import json
3
  import uuid
4
- import logging
5
  import time
 
 
 
 
6
 
 
7
  from flask import Flask, request, Response, jsonify, stream_with_context
8
- from flask_cors import CORS
9
- from dotenv import load_dotenv
10
- from curl_cffi import requests
11
 
12
- # 配置日志
13
- class CustomLogger:
14
- def __init__(self):
15
- self.logger = logging.getLogger("grok_api")
16
- self.logger.setLevel(logging.INFO)
17
-
18
- # 创建控制台处理器
19
- console_handler = logging.StreamHandler()
20
- console_handler.setLevel(logging.INFO)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
 
22
- # 设置日志格式
23
- formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
24
- console_handler.setFormatter(formatter)
25
 
26
- # 添加处理器到日志器
27
- self.logger.addHandler(console_handler)
28
-
29
- def info(self, message, component="App"):
30
- self.logger.info(f"[{component}] {message}")
31
 
32
- def error(self, message, component="App"):
33
- self.logger.error(f"[{component}] {message}")
 
34
 
35
- def request_logger(self):
36
- def middleware():
37
- # 在请求处理之前记录
38
- self.info(f"{request.method} {request.path}", "Request")
39
- # 继续处理请求
40
- return None
41
- return middleware
 
 
42
 
43
- # 加载环境变量
44
- load_dotenv()
45
 
46
- # 全局配置
47
  CONFIG = {
48
  "MODELS": {
49
  'grok-2': 'grok-latest',
@@ -56,97 +93,342 @@ CONFIG = {
56
  "grok-3-reasoning": "grok-3"
57
  },
58
  "API": {
 
59
  "BASE_URL": "https://grok.com",
60
- "API_KEY": os.getenv("API_KEY", "sk-123456"),
61
- "IS_TEMP_CONVERSATION": os.getenv("IS_TEMP_CONVERSATION", "false").lower() == "true",
62
- "PICGO_KEY": os.getenv("PICGO_KEY", None),
63
- "SIGNATURE_COOKIE": ""
 
 
64
  },
65
  "SERVER": {
66
- "PORT": int(os.getenv("PORT", 3000)),
67
- "BODY_LIMIT": "5mb"
68
  },
69
  "RETRY": {
70
- "MAX_ATTEMPTS": 2 # 重试次数
71
- },
72
- "DEFAULT_HEADERS": {
73
- 'Accept': '*/*',
74
- 'Accept-Language': 'zh-CN,zh;q=0.9',
75
- 'Accept-Encoding': 'gzip, deflate, br, zstd',
76
- 'Content-Type': 'text/plain;charset=UTF-8',
77
- 'Connection': 'keep-alive',
78
- 'Origin': 'https://grok.com',
79
- 'Priority': 'u=1, i',
80
- 'Sec-Ch-Ua': '"Chromium";v="130", "Google Chrome";v="130", "Not?A_Brand";v="99"',
81
- 'Sec-Ch-Ua-Mobile': '?0',
82
- 'Sec-Ch-Ua-Platform': '"Windows"',
83
- 'Sec-Fetch-Dest': 'empty',
84
- 'Sec-Fetch-Mode': 'cors',
85
- 'Sec-Fetch-Site': 'same-origin',
86
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36',
87
- 'Baggage': 'sentry-public_key=b311e0f2690c81f25e2c4cf6d4f7ce1c'
88
  },
89
- "SIGNATUREARRAY": [],
90
- "SIGNATUREINDEX": 0,
91
- "SHOW_THINKING": os.getenv("SHOW_THINKING", "false").lower() == "true",
92
  "IS_THINKING": False,
93
  "IS_IMG_GEN": False,
94
  "IS_IMG_GEN2": False,
95
- "ISSHOW_SEARCH_RESULTS": os.getenv("ISSHOW_SEARCH_RESULTS", "true").lower() == "true"
96
  }
97
 
98
- # 初始化Logger
99
- Logger = CustomLogger()
100
 
101
- async def initialization():
102
- """初始化函数,加载令牌"""
103
- sso_array = os.getenv("SSO", "").split(",")
104
- Logger.info("开始加载令牌", "Server")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
 
106
- for sso in sso_array:
107
- if sso.strip(): # 确保不添加空值
108
- CONFIG["SIGNATUREARRAY"].append(f"sso-rw={sso};sso={sso}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
 
110
- Logger.info(f"成功加载令牌: {json.dumps(CONFIG['SIGNATUREARRAY'], indent=2)}", "Server")
111
- Logger.info(f"令牌加载完成,共加载: {len(CONFIG['SIGNATUREARRAY'])}个令牌", "Server")
112
- Logger.info("初始化完成", "Server")
113
-
114
  class Utils:
115
  @staticmethod
116
  def organize_search_results(search_results):
117
- """格式化搜索结果"""
118
- # 确保传入的是有效的搜索结果对象
119
- if not search_results or "results" not in search_results:
120
  return ''
121
-
122
- results = search_results["results"]
123
  formatted_results = []
124
 
125
  for index, result in enumerate(results):
126
- # 处理可能为空的字段
127
- title = result.get("title", "未知标题")
128
- url = result.get("url", "#")
129
- preview = result.get("preview", "无预览内容")
130
-
131
  formatted_result = f"\r\n<details><summary>资料[{index}]: {title}</summary>\r\n{preview}\r\n\n[Link]({url})\r\n</details>"
132
  formatted_results.append(formatted_result)
133
-
134
  return '\n\n'.join(formatted_results)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
 
136
  class GrokApiClient:
137
  def __init__(self, model_id):
138
  if model_id not in CONFIG["MODELS"]:
139
  raise ValueError(f"不支持的模型: {model_id}")
140
  self.model_id = CONFIG["MODELS"][model_id]
141
-
142
  def process_message_content(self, content):
143
- """处理消息内容"""
144
  if isinstance(content, str):
145
  return content
146
  return None
147
-
148
  def get_image_type(self, base64_string):
149
- """获取图片类型"""
150
  mime_type = 'image/jpeg'
151
  if 'data:image' in base64_string:
152
  import re
@@ -156,94 +438,92 @@ class GrokApiClient:
156
 
157
  extension = mime_type.split('/')[1]
158
  file_name = f"image.{extension}"
159
-
160
  return {
161
  "mimeType": mime_type,
162
  "fileName": file_name
163
  }
164
-
165
- async def upload_base64_image(self, base64_data, url):
166
- """上传Base64图片"""
167
  try:
168
- # 处理 base64 数据
169
  if 'data:image' in base64_data:
170
  image_buffer = base64_data.split(',')[1]
171
  else:
172
  image_buffer = base64_data
173
 
174
  image_info = self.get_image_type(base64_data)
 
 
 
175
  upload_data = {
176
  "rpc": "uploadFile",
177
  "req": {
178
- "fileName": image_info["fileName"],
179
- "fileMimeType": image_info["mimeType"],
180
  "content": image_buffer
181
  }
182
  }
183
 
184
- Logger.info("发送图片请求", "Server")
185
 
186
- # 使用curl_cffi发送请求
187
- response = requests.post(
188
  url,
189
  headers={
190
- **CONFIG["DEFAULT_HEADERS"],
191
- "cookie": CONFIG["API"]["SIGNATURE_COOKIE"]
192
  },
193
- json=upload_data
 
 
194
  )
195
-
196
  if response.status_code != 200:
197
- Logger.error(f"上传图片失败,状态码:{response.status_code}", "Server")
198
  return ''
199
-
200
  result = response.json()
201
- Logger.info(f"上传图片成功: {result}", "Server")
202
- return result["fileMetadataId"]
203
-
204
  except Exception as error:
205
- Logger.error(str(error), "Server")
206
  return ''
207
-
208
- async def prepare_chat_request(self, request_data):
209
- """准备聊天请求"""
210
- todo_messages = request_data["messages"]
211
- if request_data["model"] in ['grok-2-imageGen', 'grok-3-imageGen']:
 
 
 
 
212
  last_message = todo_messages[-1]
213
  if last_message["role"] != 'user':
214
- raise ValueError('画图模型的最后一条消息必须是用户消息!')
215
  todo_messages = [last_message]
216
-
217
  file_attachments = []
218
  messages = ''
219
  last_role = None
220
  last_content = ''
221
- search = request_data["model"] in ['grok-2-search', 'grok-3-search']
222
-
223
  # 移除<think>标签及其内容和base64图片
224
  def remove_think_tags(text):
225
  import re
226
  text = re.sub(r'<think>[\s\S]*?<\/think>', '', text).strip()
227
  text = re.sub(r'!\[image\]\(data:.*?base64,.*?\)', '[图片]', text)
228
  return text
229
-
230
- async def process_image_url(content):
231
- if content["type"] == 'image_url' and 'data:image' in content["image_url"]["url"]:
232
- image_response = await self.upload_base64_image(
233
- content["image_url"]["url"],
234
- f"{CONFIG['API']['BASE_URL']}/api/rpc"
235
- )
236
- return image_response
237
- return None
238
-
239
- async def process_content(content):
240
  if isinstance(content, list):
241
  text_content = ''
242
  for item in content:
243
  if item["type"] == 'image_url':
244
- text_content += (text_content + '\n' if text_content else '') + "[图片]"
245
  elif item["type"] == 'text':
246
- text_content += (text_content + '\n' if text_content else '') + remove_think_tags(item["text"])
247
  return text_content
248
  elif isinstance(content, dict) and content is not None:
249
  if content["type"] == 'image_url':
@@ -251,27 +531,32 @@ class GrokApiClient:
251
  elif content["type"] == 'text':
252
  return remove_think_tags(content["text"])
253
  return remove_think_tags(self.process_message_content(content))
254
-
255
  for current in todo_messages:
256
  role = 'assistant' if current["role"] == 'assistant' else 'user'
257
  is_last_message = current == todo_messages[-1]
258
-
259
- # 处理图片附件
260
  if is_last_message and "content" in current:
261
  if isinstance(current["content"], list):
262
  for item in current["content"]:
263
- if item.get("type") == 'image_url':
264
- processed_image = await process_image_url(item)
 
 
 
265
  if processed_image:
266
  file_attachments.append(processed_image)
267
  elif isinstance(current["content"], dict) and current["content"].get("type") == 'image_url':
268
- processed_image = await process_image_url(current["content"])
 
 
 
269
  if processed_image:
270
  file_attachments.append(processed_image)
 
271
 
272
- # 处理文本内容
273
- text_content = await process_content(current.get("content", ""))
274
-
275
  if text_content or (is_last_message and file_attachments):
276
  if role == last_role and text_content:
277
  last_content += '\n' + text_content
@@ -280,9 +565,9 @@ class GrokApiClient:
280
  messages += f"{role.upper()}: {text_content or '[图片]'}\n"
281
  last_content = text_content
282
  last_role = role
283
-
284
  return {
285
- "temporary": CONFIG["API"]["IS_TEMP_CONVERSATION"],
286
  "modelName": self.model_id,
287
  "message": messages.strip(),
288
  "fileAttachments": file_attachments[:4],
@@ -295,7 +580,7 @@ class GrokApiClient:
295
  "imageGenerationCount": 1,
296
  "forceConcise": False,
297
  "toolOverrides": {
298
- "imageGen": request_data["model"] in ['grok-2-imageGen', 'grok-3-imageGen'],
299
  "webSearch": search,
300
  "xSearch": search,
301
  "xMediaSearch": search,
@@ -306,20 +591,19 @@ class GrokApiClient:
306
  "isPreset": False,
307
  "sendFinalMetadata": True,
308
  "customInstructions": "",
309
- "deepsearchPreset": "default" if request_data["model"] == 'grok-3-deepsearch' else "",
310
- "isReasoning": request_data["model"] == 'grok-3-reasoning'
311
  }
312
 
313
  class MessageProcessor:
314
  @staticmethod
315
  def create_chat_response(message, model, is_stream=False):
316
- """创建聊天响应"""
317
  base_response = {
318
- "id": f"chatcmpl-{str(uuid.uuid4())}",
319
  "created": int(time.time()),
320
  "model": model
321
  }
322
-
323
  if is_stream:
324
  return {
325
  **base_response,
@@ -347,220 +631,302 @@ class MessageProcessor:
347
  }
348
 
349
  def process_model_response(response, model):
350
- """处理模型响应"""
351
  result = {"token": None, "imageUrl": None}
352
 
353
  if CONFIG["IS_IMG_GEN"]:
354
- if response and response.get("cachedImageGenerationResponse") and not CONFIG["IS_IMG_GEN2"]:
355
  result["imageUrl"] = response["cachedImageGenerationResponse"]["imageUrl"]
356
  return result
357
-
358
- # 非生图模型的处理
359
  if model == 'grok-2':
360
  result["token"] = response.get("token")
361
  elif model in ['grok-2-search', 'grok-3-search']:
362
- if response and response.get("webSearchResults") and CONFIG["ISSHOW_SEARCH_RESULTS"]:
363
  result["token"] = f"\r\n<think>{Utils.organize_search_results(response['webSearchResults'])}</think>\r\n"
364
  else:
365
  result["token"] = response.get("token")
366
  elif model == 'grok-3':
367
  result["token"] = response.get("token")
368
  elif model == 'grok-3-deepsearch':
369
- if response and response.get("messageTag") == "final":
370
- result["token"] = response.get("token")
 
 
 
 
 
 
 
 
371
  elif model == 'grok-3-reasoning':
372
- if response and response.get("isThinking") and not CONFIG["SHOW_THINKING"]:
373
  return result
374
-
375
- if response and response.get("isThinking") and not CONFIG["IS_THINKING"]:
376
  result["token"] = "<think>" + response.get("token", "")
377
  CONFIG["IS_THINKING"] = True
378
- elif response and not response.get("isThinking") and CONFIG["IS_THINKING"]:
379
  result["token"] = "</think>" + response.get("token", "")
380
  CONFIG["IS_THINKING"] = False
381
  else:
382
  result["token"] = response.get("token")
383
-
384
  return result
385
 
386
- async def handle_stream_response(response, model, flask_response):
387
- """处理流式响应"""
388
- try:
389
- stream = response.iter_lines()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
390
 
391
- CONFIG["IS_THINKING"] = False
392
- CONFIG["IS_IMG_GEN"] = False
393
- CONFIG["IS_IMG_GEN2"] = False
394
- Logger.info("开始处理流式响应", "Server")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
395
 
396
- def generate():
397
- for line in stream:
398
- if not line:
399
- continue
400
- line_json = json.loads(line.decode("utf-8").strip())
401
- try:
402
- if line_json and line_json.get("error"):
403
- raise ValueError("RateLimitError")
404
-
405
- response_data = line_json.get("result", {}).get("response")
406
- if not response_data:
407
- continue
408
-
409
- if response_data.get("doImgGen") or response_data.get("imageAttachmentInfo"):
410
- CONFIG["IS_IMG_GEN"] = True
411
-
412
- result = process_model_response(response_data, model)
413
-
414
- if result["token"]:
415
- chat_response = MessageProcessor.create_chat_response(result["token"], model, True)
416
- yield f"data: {json.dumps(chat_response)}\n\n"
417
-
418
- if result["imageUrl"]:
419
- CONFIG["IS_IMG_GEN2"] = True
420
- data_image = handle_image_response(result["imageUrl"])
421
- image_response = MessageProcessor.create_chat_response(data_image, model, True)
422
- yield f"data: {json.dumps(image_response)}\n\n"
423
-
424
- except Exception as error:
425
- Logger.error(str(error), "Server")
426
- continue
427
-
428
- yield "data: [DONE]\n\n"
429
 
430
- return Response(generate(), mimetype="text/event-stream")
431
- except Exception as error:
432
- Logger.error(str(error), "Server")
433
- raise error
434
 
435
- async def handle_non_stream_response(response, model):
436
- """处理非流式响应"""
437
  try:
438
- stream = response.iter_lines()
 
439
  full_response = ""
440
-
441
  CONFIG["IS_THINKING"] = False
442
  CONFIG["IS_IMG_GEN"] = False
443
  CONFIG["IS_IMG_GEN2"] = False
444
- Logger.info("开始处理非流式响应", "Server")
445
-
446
- for line in stream:
447
- if not line:
 
448
  continue
449
-
450
  try:
451
- line_json = json.loads(line.decode("utf-8").strip())
452
- if line_json and line_json.get("error"):
 
 
 
453
  raise ValueError("RateLimitError")
454
-
455
  response_data = line_json.get("result", {}).get("response")
456
  if not response_data:
457
  continue
458
-
459
  if response_data.get("doImgGen") or response_data.get("imageAttachmentInfo"):
460
  CONFIG["IS_IMG_GEN"] = True
461
-
462
  result = process_model_response(response_data, model)
463
 
464
  if result["token"]:
465
  full_response += result["token"]
466
-
467
  if result["imageUrl"]:
468
  CONFIG["IS_IMG_GEN2"] = True
469
- data_image = await handle_image_response(result["imageUrl"])
470
- return MessageProcessor.create_chat_response(data_image, model)
471
-
472
- except Exception as error:
473
- Logger.error(str(error), "Server")
474
  continue
475
-
476
- if not CONFIG["IS_IMG_GEN2"]:
477
- return MessageProcessor.create_chat_response(full_response, model)
478
- return None
 
 
479
  except Exception as error:
480
- Logger.error(str(error), "Server")
481
- raise error
482
 
483
- async def handle_image_response(image_url):
484
- """处理图片响应"""
485
- MAX_RETRIES = 2
486
- retry_count = 0
487
-
488
- while retry_count < MAX_RETRIES:
489
- try:
490
- # 使用curl_cffi获取图片
491
- image_response = requests.get(
492
- f"https://assets.grok.com/{image_url}",
493
- headers={
494
- **CONFIG["DEFAULT_HEADERS"],
495
- "Cookie": CONFIG["API"]["SIGNATURE_COOKIE"]
496
- }
497
- )
498
 
499
- if image_response.status_code == 200:
500
- break
 
 
 
 
 
 
 
 
 
 
 
 
501
 
502
- retry_count += 1
503
- if retry_count == MAX_RETRIES:
504
- raise ValueError(f"上游服务请求失败! status: {image_response.status_code}")
505
 
506
- time.sleep(1 * retry_count) # 简单的退避策略
507
-
508
- except Exception as error:
509
- Logger.error(str(error), "Server")
510
- retry_count += 1
511
- if retry_count == MAX_RETRIES:
512
- raise error
513
 
514
- time.sleep(1 * retry_count)
 
 
 
 
 
 
 
 
 
 
 
 
515
 
516
- # 图片处理逻辑
517
- image_buffer = image_response.content
 
 
 
 
518
 
519
- if CONFIG["API"]["PICGO_KEY"]:
520
- # 使用curl_cffi上传到PICGO
521
- import io
522
- from curl_cffi.requests import AsyncSession
523
-
524
- files = {
525
- "source": ("image.jpg", image_buffer, "image/jpeg")
526
- }
527
-
528
- headers = {
529
- "X-API-Key": CONFIG["API"]["PICGO_KEY"]
530
- }
531
-
532
- try:
533
- pic_response = requests.post(
534
- "https://www.picgo.net/api/1/upload",
535
- headers=headers,
536
- files=files
537
- )
538
-
539
- if pic_response.status_code != 200:
540
- return "生图失败,请查看PICGO图床密钥是否设置正确"
541
-
542
- Logger.info("生图成功", "Server")
543
- result = pic_response.json()
544
- return f"![image]({result['image']['url']})"
545
- except Exception as e:
546
- Logger.error(f"上传PICGO失败: {str(e)}", "Server")
547
- return "生图上传失败,请检查网络连接和PICGO配置"
548
 
549
- # 如果没有PICGO_KEY,可以返回一个适当的消息或其他逻辑
550
- return "生图成功,但未配置PICGO图床,无法显示图片"
 
 
 
551
 
552
- # 创建Flask应用
553
  app = Flask(__name__)
554
- CORS(app, resources={r"/*": {"origins": "*", "methods": ["GET", "POST", "OPTIONS"], "allow_headers": ["Content-Type", "Authorization"]}})
 
555
 
556
  @app.before_request
557
  def log_request_info():
558
- """请求日志中间件"""
559
- Logger.info(f"{request.method} {request.path}", "Request")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
560
 
561
  @app.route('/v1/models', methods=['GET'])
562
- async def get_models():
563
- """获取模型列表"""
564
  return jsonify({
565
  "object": "list",
566
  "data": [
@@ -569,81 +935,108 @@ async def get_models():
569
  "object": "model",
570
  "created": int(time.time()),
571
  "owned_by": "grok"
572
- } for model in CONFIG["MODELS"].keys()
 
573
  ]
574
  })
575
 
576
  @app.route('/v1/chat/completions', methods=['POST'])
577
- async def chat_completions():
578
- """处理聊天完成请求"""
579
  try:
580
- # 验证API密钥
581
  auth_token = request.headers.get('Authorization', '').replace('Bearer ', '')
582
- if auth_token != CONFIG["API"]["API_KEY"]:
583
- return jsonify({"error": "Unauthorized"}), 401
584
-
585
- # 解析请求
586
- data = request.get_json()
 
 
 
 
 
587
  model = data.get("model")
588
  stream = data.get("stream", False)
589
 
590
- # 创建Grok客户端
591
  grok_client = GrokApiClient(model)
592
- request_payload = await grok_client.prepare_chat_request(data)
593
- Logger.info(json.dumps(request_payload, indent=2), "Server")
594
 
595
- # 重试逻辑
596
- retry_count = 0
597
  while retry_count < CONFIG["RETRY"]["MAX_ATTEMPTS"]:
598
  retry_count += 1
599
- Logger.info("开始请求", "Server")
600
 
601
- # 设置cookie
602
- CONFIG["API"]["SIGNATURE_COOKIE"] = CONFIG["SIGNATUREARRAY"][CONFIG["SIGNATUREINDEX"]]
603
 
604
- # 发送请求
605
- response = requests.post(
606
- f"{CONFIG['API']['BASE_URL']}/rest/app-chat/conversations/new",
607
- headers={
608
- "Accept": "text/event-stream",
609
- "Baggage": "sentry-public_key=b311e0f2690c81f25e2c4cf6d4f7ce1c",
610
- "Content-Type": "text/plain;charset=UTF-8",
611
- "Connection": "keep-alive",
612
- "Cookie": CONFIG["API"]["SIGNATURE_COOKIE"]
613
- },
614
- data=json.dumps(request_payload),
615
- impersonate="chrome110",
616
- stream=True
617
- )
618
 
619
- if response.status_code == 200:
620
- Logger.info("请求成功", "Server")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
621
 
622
- if stream:
623
- # 处理流式响应
624
- stream_response = await handle_stream_response(response, model, Response())
625
- CONFIG["SIGNATUREINDEX"] = (CONFIG["SIGNATUREINDEX"] + 1) % len(CONFIG["SIGNATUREARRAY"])
626
- return stream_response
627
  else:
628
- # 处理非流式响应
629
- non_stream_result = await handle_non_stream_response(response, model)
630
- CONFIG["SIGNATUREINDEX"] = (CONFIG["SIGNATUREINDEX"] + 1) % len(CONFIG["SIGNATUREARRAY"])
631
- if non_stream_result:
632
- return jsonify(non_stream_result)
633
 
634
- # 轮换token
635
- CONFIG["SIGNATUREINDEX"] = (CONFIG["SIGNATUREINDEX"] + 1) % len(CONFIG["SIGNATUREARRAY"])
636
-
637
- # 如果所有重试都失败
638
- return jsonify({
639
- "error": {
640
- "message": "请求失败,所有令牌均已尝试",
641
- "type": "server_error"
642
- }
643
- }), 500
644
 
 
 
 
 
 
 
 
 
645
  except Exception as error:
646
- Logger.error(str(error), "ChatAPI")
647
  return jsonify({
648
  "error": {
649
  "message": str(error),
@@ -654,16 +1047,14 @@ async def chat_completions():
654
  @app.route('/', defaults={'path': ''})
655
  @app.route('/<path:path>')
656
  def catch_all(path):
657
- """处理所有其他路由"""
658
- return "api运行正常", 200
659
-
660
- # 主程序入口
661
- if __name__ == "__main__":
662
- # 初始化应用
663
- import asyncio
664
- asyncio.run(initialization())
665
-
666
- # 启动服务器
667
- Logger.info(f"服务器已启动,监听端口: {CONFIG['SERVER']['PORT']}", "Server")
668
- from waitress import serve
669
- serve(app, host="0.0.0.0", port=CONFIG["SERVER"]["PORT"])
 
1
  import os
2
  import json
3
  import uuid
 
4
  import time
5
+ import base64
6
+ import sys
7
+ import inspect
8
+ from loguru import logger
9
 
10
+ import requests
11
  from flask import Flask, request, Response, jsonify, stream_with_context
12
+ from curl_cffi import requests as curl_requests
13
+ from werkzeug.middleware.proxy_fix import ProxyFix
 
14
 
15
+
16
+ class Logger:
17
+ def __init__(self, level="INFO", colorize=True, format=None):
18
+ logger.remove()
19
+
20
+ if format is None:
21
+ format = (
22
+ "<green>{time:YYYY-MM-DD HH:mm:ss}</green> | "
23
+ "<level>{level: <8}</level> | "
24
+ "<cyan>{extra[filename]}</cyan>:<cyan>{extra[function]}</cyan>:<cyan>{extra[lineno]}</cyan> | "
25
+ "<level>{message}</level>"
26
+ )
27
+
28
+ logger.add(
29
+ sys.stderr,
30
+ level=level,
31
+ format=format,
32
+ colorize=colorize,
33
+ backtrace=True,
34
+ diagnose=True
35
+ )
36
+
37
+ self.logger = logger
38
+
39
+ def _get_caller_info(self):
40
+ frame = inspect.currentframe()
41
+ try:
42
+ caller_frame = frame.f_back.f_back
43
+ full_path = caller_frame.f_code.co_filename
44
+ function = caller_frame.f_code.co_name
45
+ lineno = caller_frame.f_lineno
46
+
47
+ filename = os.path.basename(full_path)
48
+
49
+ return {
50
+ 'filename': filename,
51
+ 'function': function,
52
+ 'lineno': lineno
53
+ }
54
+ finally:
55
+ del frame
56
+
57
+ def info(self, message, source="API"):
58
+ caller_info = self._get_caller_info()
59
+ self.logger.bind(**caller_info).info(f"[{source}] {message}")
60
 
61
+ def error(self, message, source="API"):
62
+ caller_info = self._get_caller_info()
 
63
 
64
+ if isinstance(message, Exception):
65
+ self.logger.bind(**caller_info).exception(f"[{source}] {str(message)}")
66
+ else:
67
+ self.logger.bind(**caller_info).error(f"[{source}] {message}")
 
68
 
69
+ def warning(self, message, source="API"):
70
+ caller_info = self._get_caller_info()
71
+ self.logger.bind(**caller_info).warning(f"[{source}] {message}")
72
 
73
+ def debug(self, message, source="API"):
74
+ caller_info = self._get_caller_info()
75
+ self.logger.bind(**caller_info).debug(f"[{source}] {message}")
76
+
77
+ async def request_logger(self, request):
78
+ caller_info = self._get_caller_info()
79
+ self.logger.bind(**caller_info).info(f"请求: {request.method} {request.path}", "Request")
80
+
81
+ logger = Logger(level="INFO")
82
 
 
 
83
 
 
84
  CONFIG = {
85
  "MODELS": {
86
  'grok-2': 'grok-latest',
 
93
  "grok-3-reasoning": "grok-3"
94
  },
95
  "API": {
96
+ "IS_CUSTOM_SSO": os.environ.get("IS_CUSTOM_SSO", "false").lower() == "true",
97
  "BASE_URL": "https://grok.com",
98
+ "API_KEY": os.environ.get("API_KEY", "sk-123456"),
99
+ "SIGNATURE_COOKIE": None,
100
+ "PICGO_KEY": os.environ.get("PICGO_KEY") or None,
101
+ "TUMY_KEY": os.environ.get("TUMY_KEY") or None,
102
+ "RETRY_TIME": 1000,
103
+ "PROXY": os.environ.get("PROXY") or None
104
  },
105
  "SERVER": {
106
+ "PORT": int(os.environ.get("PORT", 5200))
 
107
  },
108
  "RETRY": {
109
+ "MAX_ATTEMPTS": 2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
  },
111
+ "SHOW_THINKING": os.environ.get("SHOW_THINKING") == "true",
 
 
112
  "IS_THINKING": False,
113
  "IS_IMG_GEN": False,
114
  "IS_IMG_GEN2": False,
115
+ "ISSHOW_SEARCH_RESULTS": os.environ.get("ISSHOW_SEARCH_RESULTS", "true").lower() == "true"
116
  }
117
 
 
 
118
 
119
+ DEFAULT_HEADERS = {
120
+ 'Accept': '*/*',
121
+ 'Accept-Language': 'zh-CN,zh;q=0.9',
122
+ 'Accept-Encoding': 'gzip, deflate, br, zstd',
123
+ 'Content-Type': 'text/plain;charset=UTF-8',
124
+ 'Connection': 'keep-alive',
125
+ 'Origin': 'https://grok.com',
126
+ 'Priority': 'u=1, i',
127
+ 'Sec-Ch-Ua': '"Chromium";v="130", "Google Chrome";v="130", "Not?A_Brand";v="99"',
128
+ 'Sec-Ch-Ua-Mobile': '?0',
129
+ 'Sec-Ch-Ua-Platform': '"Windows"',
130
+ 'Sec-Fetch-Dest': 'empty',
131
+ 'Sec-Fetch-Mode': 'cors',
132
+ 'Sec-Fetch-Site': 'same-origin',
133
+ 'Baggage': 'sentry-public_key=b311e0f2690c81f25e2c4cf6d4f7ce1c'
134
+ }
135
+
136
+ class AuthTokenManager:
137
+ def __init__(self):
138
+ self.token_model_map = {}
139
+ self.expired_tokens = set()
140
+ self.token_status_map = {}
141
+
142
+ self.model_config = {
143
+ "grok-2": {
144
+ "RequestFrequency": 30,
145
+ "ExpirationTime": 1 * 60 * 60 * 1000 # 1小时
146
+ },
147
+ "grok-3": {
148
+ "RequestFrequency": 20,
149
+ "ExpirationTime": 2 * 60 * 60 * 1000 # 2小时
150
+ },
151
+ "grok-3-deepsearch": {
152
+ "RequestFrequency": 10,
153
+ "ExpirationTime": 24 * 60 * 60 * 1000 # 24小时
154
+ },
155
+ "grok-3-reasoning": {
156
+ "RequestFrequency": 10,
157
+ "ExpirationTime": 24 * 60 * 60 * 1000 # 24小时
158
+ }
159
+ }
160
+ self.token_reset_switch = False
161
+ self.token_reset_timer = None
162
+
163
+ def add_token(self, token):
164
+ sso = token.split("sso=")[1].split(";")[0]
165
+ for model in self.model_config.keys():
166
+ if model not in self.token_model_map:
167
+ self.token_model_map[model] = []
168
+ if sso not in self.token_status_map:
169
+ self.token_status_map[sso] = {}
170
+
171
+ existing_token_entry = next((entry for entry in self.token_model_map[model] if entry["token"] == token), None)
172
+
173
+ if not existing_token_entry:
174
+ self.token_model_map[model].append({
175
+ "token": token,
176
+ "RequestCount": 0,
177
+ "AddedTime": int(time.time() * 1000),
178
+ "StartCallTime": None
179
+ })
180
+
181
+ if model not in self.token_status_map[sso]:
182
+ self.token_status_map[sso][model] = {
183
+ "isValid": True,
184
+ "invalidatedTime": None,
185
+ "totalRequestCount": 0
186
+ }
187
 
188
+ def set_token(self, token):
189
+ models = list(self.model_config.keys())
190
+ self.token_model_map = {model: [{
191
+ "token": token,
192
+ "RequestCount": 0,
193
+ "AddedTime": int(time.time() * 1000),
194
+ "StartCallTime": None
195
+ }] for model in models}
196
+
197
+ sso = token.split("sso=")[1].split(";")[0]
198
+ self.token_status_map[sso] = {model: {
199
+ "isValid": True,
200
+ "invalidatedTime": None,
201
+ "totalRequestCount": 0
202
+ } for model in models}
203
+
204
+ def delete_token(self, token):
205
+ try:
206
+ sso = token.split("sso=")[1].split(";")[0]
207
+ for model in self.token_model_map:
208
+ self.token_model_map[model] = [entry for entry in self.token_model_map[model] if entry["token"] != token]
209
+
210
+ if sso in self.token_status_map:
211
+ del self.token_status_map[sso]
212
+
213
+ logger.info(f"令牌已成功移除: {token}", "TokenManager")
214
+ return True
215
+ except Exception as error:
216
+ logger.error(f"令牌删除失败: {str(error)}")
217
+ return False
218
+
219
+ def get_next_token_for_model(self, model_id):
220
+ normalized_model = self.normalize_model_name(model_id)
221
+
222
+ if normalized_model not in self.token_model_map or not self.token_model_map[normalized_model]:
223
+ return None
224
+
225
+ token_entry = self.token_model_map[normalized_model][0]
226
+
227
+ if token_entry:
228
+ if token_entry["StartCallTime"] is None:
229
+ token_entry["StartCallTime"] = int(time.time() * 1000)
230
+
231
+ if not self.token_reset_switch:
232
+ self.start_token_reset_process()
233
+ self.token_reset_switch = True
234
+
235
+ token_entry["RequestCount"] += 1
236
+
237
+ if token_entry["RequestCount"] > self.model_config[normalized_model]["RequestFrequency"]:
238
+ self.remove_token_from_model(normalized_model, token_entry["token"])
239
+ next_token_entry = self.token_model_map[normalized_model][0] if self.token_model_map[normalized_model] else None
240
+ return next_token_entry["token"] if next_token_entry else None
241
+
242
+ sso = token_entry["token"].split("sso=")[1].split(";")[0]
243
+ if sso in self.token_status_map and normalized_model in self.token_status_map[sso]:
244
+ if token_entry["RequestCount"] == self.model_config[normalized_model]["RequestFrequency"]:
245
+ self.token_status_map[sso][normalized_model]["isValid"] = False
246
+ self.token_status_map[sso][normalized_model]["invalidatedTime"] = int(time.time() * 1000)
247
+ self.token_status_map[sso][normalized_model]["totalRequestCount"] += 1
248
+
249
+ return token_entry["token"]
250
+
251
+ return None
252
+
253
+ def remove_token_from_model(self, model_id, token):
254
+ normalized_model = self.normalize_model_name(model_id)
255
+
256
+ if normalized_model not in self.token_model_map:
257
+ logger.error(f"模型 {normalized_model} 不存在", "TokenManager")
258
+ return False
259
+
260
+ model_tokens = self.token_model_map[normalized_model]
261
+ token_index = next((i for i, entry in enumerate(model_tokens) if entry["token"] == token), -1)
262
+
263
+ if token_index != -1:
264
+ removed_token_entry = model_tokens.pop(token_index)
265
+ self.expired_tokens.add((
266
+ removed_token_entry["token"],
267
+ normalized_model,
268
+ int(time.time() * 1000)
269
+ ))
270
+
271
+ if not self.token_reset_switch:
272
+ self.start_token_reset_process()
273
+ self.token_reset_switch = True
274
+
275
+ logger.info(f"模型{model_id}的令牌已失效,已成功移除令牌: {token}", "TokenManager")
276
+ return True
277
+
278
+ logger.error(f"在模型 {normalized_model} 中未找到 token: {token}", "TokenManager")
279
+ return False
280
+
281
+ def get_expired_tokens(self):
282
+ return list(self.expired_tokens)
283
+
284
+ def normalize_model_name(self, model):
285
+ if model.startswith('grok-') and 'deepsearch' not in model and 'reasoning' not in model:
286
+ return '-'.join(model.split('-')[:2])
287
+ return model
288
+
289
+ def get_token_count_for_model(self, model_id):
290
+ normalized_model = self.normalize_model_name(model_id)
291
+ return len(self.token_model_map.get(normalized_model, []))
292
+
293
+ def get_remaining_token_request_capacity(self):
294
+ remaining_capacity_map = {}
295
+
296
+ for model in self.model_config.keys():
297
+ model_tokens = self.token_model_map.get(model, [])
298
+ model_request_frequency = self.model_config[model]["RequestFrequency"]
299
+
300
+ total_used_requests = sum(token_entry.get("RequestCount", 0) for token_entry in model_tokens)
301
+
302
+ remaining_capacity = (len(model_tokens) * model_request_frequency) - total_used_requests
303
+ remaining_capacity_map[model] = max(0, remaining_capacity)
304
+
305
+ return remaining_capacity_map
306
+
307
+ def get_token_array_for_model(self, model_id):
308
+ normalized_model = self.normalize_model_name(model_id)
309
+ return self.token_model_map.get(normalized_model, [])
310
+
311
+ def start_token_reset_process(self):
312
+ def reset_expired_tokens():
313
+ now = int(time.time() * 1000)
314
+
315
+ tokens_to_remove = set()
316
+ for token_info in self.expired_tokens:
317
+ token, model, expired_time = token_info
318
+ expiration_time = self.model_config[model]["ExpirationTime"]
319
+
320
+ if now - expired_time >= expiration_time:
321
+ if not any(entry["token"] == token for entry in self.token_model_map.get(model, [])):
322
+ if model not in self.token_model_map:
323
+ self.token_model_map[model] = []
324
+
325
+ self.token_model_map[model].append({
326
+ "token": token,
327
+ "RequestCount": 0,
328
+ "AddedTime": now,
329
+ "StartCallTime": None
330
+ })
331
+
332
+ sso = token.split("sso=")[1].split(";")[0]
333
+ if sso in self.token_status_map and model in self.token_status_map[sso]:
334
+ self.token_status_map[sso][model]["isValid"] = True
335
+ self.token_status_map[sso][model]["invalidatedTime"] = None
336
+ self.token_status_map[sso][model]["totalRequestCount"] = 0
337
+
338
+ tokens_to_remove.add(token_info)
339
+
340
+ self.expired_tokens -= tokens_to_remove
341
+
342
+ for model in self.model_config.keys():
343
+ if model not in self.token_model_map:
344
+ continue
345
+
346
+ for token_entry in self.token_model_map[model]:
347
+ if not token_entry.get("StartCallTime"):
348
+ continue
349
+
350
+ expiration_time = self.model_config[model]["ExpirationTime"]
351
+ if now - token_entry["StartCallTime"] >= expiration_time:
352
+ sso = token_entry["token"].split("sso=")[1].split(";")[0]
353
+ if sso in self.token_status_map and model in self.token_status_map[sso]:
354
+ self.token_status_map[sso][model]["isValid"] = True
355
+ self.token_status_map[sso][model]["invalidatedTime"] = None
356
+ self.token_status_map[sso][model]["totalRequestCount"] = 0
357
+
358
+ token_entry["RequestCount"] = 0
359
+ token_entry["StartCallTime"] = None
360
+
361
+ import threading
362
+ # 启动一个线程执行定时任务,每小时执行一次
363
+ def run_timer():
364
+ while True:
365
+ reset_expired_tokens()
366
+ time.sleep(3600)
367
+
368
+ timer_thread = threading.Thread(target=run_timer)
369
+ timer_thread.daemon = True
370
+ timer_thread.start()
371
+
372
+ def get_all_tokens(self):
373
+ all_tokens = set()
374
+ for model_tokens in self.token_model_map.values():
375
+ for entry in model_tokens:
376
+ all_tokens.add(entry["token"])
377
+ return list(all_tokens)
378
+
379
+ def get_token_status_map(self):
380
+ return self.token_status_map
381
 
 
 
 
 
382
  class Utils:
383
  @staticmethod
384
  def organize_search_results(search_results):
385
+ if not search_results or 'results' not in search_results:
 
 
386
  return ''
387
+
388
+ results = search_results['results']
389
  formatted_results = []
390
 
391
  for index, result in enumerate(results):
392
+ title = result.get('title', '未知标题')
393
+ url = result.get('url', '#')
394
+ preview = result.get('preview', '无预览内容')
395
+
 
396
  formatted_result = f"\r\n<details><summary>资料[{index}]: {title}</summary>\r\n{preview}\r\n\n[Link]({url})\r\n</details>"
397
  formatted_results.append(formatted_result)
398
+
399
  return '\n\n'.join(formatted_results)
400
+
401
+ @staticmethod
402
+ def create_auth_headers(model):
403
+ return token_manager.get_next_token_for_model(model)
404
+
405
+ @staticmethod
406
+ def get_proxy_options():
407
+ proxy = CONFIG["API"]["PROXY"]
408
+ proxy_options = {}
409
+
410
+ if proxy:
411
+ logger.info(f"使用代理: {proxy}", "Server")
412
+ proxy_options["proxies"] = {"https": proxy, "http": proxy}
413
+
414
+ if proxy.startswith("socks5://"):
415
+ proxy_options["proxies"] = {"https": proxy, "http": proxy}
416
+ proxy_options["proxy_type"] = "socks5"
417
+
418
+ return proxy_options
419
 
420
  class GrokApiClient:
421
  def __init__(self, model_id):
422
  if model_id not in CONFIG["MODELS"]:
423
  raise ValueError(f"不支持的模型: {model_id}")
424
  self.model_id = CONFIG["MODELS"][model_id]
425
+
426
  def process_message_content(self, content):
 
427
  if isinstance(content, str):
428
  return content
429
  return None
430
+
431
  def get_image_type(self, base64_string):
 
432
  mime_type = 'image/jpeg'
433
  if 'data:image' in base64_string:
434
  import re
 
438
 
439
  extension = mime_type.split('/')[1]
440
  file_name = f"image.{extension}"
441
+
442
  return {
443
  "mimeType": mime_type,
444
  "fileName": file_name
445
  }
446
+
447
+ def upload_base64_image(self, base64_data, url):
 
448
  try:
 
449
  if 'data:image' in base64_data:
450
  image_buffer = base64_data.split(',')[1]
451
  else:
452
  image_buffer = base64_data
453
 
454
  image_info = self.get_image_type(base64_data)
455
+ mime_type = image_info["mimeType"]
456
+ file_name = image_info["fileName"]
457
+
458
  upload_data = {
459
  "rpc": "uploadFile",
460
  "req": {
461
+ "fileName": file_name,
462
+ "fileMimeType": mime_type,
463
  "content": image_buffer
464
  }
465
  }
466
 
467
+ logger.info("发送图片请求", "Server")
468
 
469
+ proxy_options = Utils.get_proxy_options()
470
+ response = curl_requests.post(
471
  url,
472
  headers={
473
+ **DEFAULT_HEADERS,
474
+ "Cookie": CONFIG["API"]["SIGNATURE_COOKIE"]
475
  },
476
+ json=upload_data,
477
+ impersonate="chrome120",
478
+ **proxy_options
479
  )
480
+
481
  if response.status_code != 200:
482
+ logger.error(f"上传图片失败,状态码:{response.status_code}", "Server")
483
  return ''
484
+
485
  result = response.json()
486
+ logger.info(f"上传图片成功: {result}", "Server")
487
+ return result.get("fileMetadataId", "")
488
+
489
  except Exception as error:
490
+ logger.error(str(error), "Server")
491
  return ''
492
+
493
+ def prepare_chat_request(self, request):
494
+ if ((request["model"] == 'grok-2-imageGen' or request["model"] == 'grok-3-imageGen') and
495
+ not CONFIG["API"]["PICGO_KEY"] and not CONFIG["API"]["TUMY_KEY"] and
496
+ request.get("stream", False)):
497
+ raise ValueError("该模型流式输出需要配置PICGO或者TUMY图床密钥!")
498
+
499
+ todo_messages = request["messages"]
500
+ if request["model"] in ['grok-2-imageGen', 'grok-3-imageGen', 'grok-3-deepsearch']:
501
  last_message = todo_messages[-1]
502
  if last_message["role"] != 'user':
503
+ raise ValueError('此模型最后一条消息必须是用户消息!')
504
  todo_messages = [last_message]
505
+
506
  file_attachments = []
507
  messages = ''
508
  last_role = None
509
  last_content = ''
510
+ search = request["model"] in ['grok-2-search', 'grok-3-search']
511
+
512
  # 移除<think>标签及其内容和base64图片
513
  def remove_think_tags(text):
514
  import re
515
  text = re.sub(r'<think>[\s\S]*?<\/think>', '', text).strip()
516
  text = re.sub(r'!\[image\]\(data:.*?base64,.*?\)', '[图片]', text)
517
  return text
518
+
519
+ def process_content(content):
 
 
 
 
 
 
 
 
 
520
  if isinstance(content, list):
521
  text_content = ''
522
  for item in content:
523
  if item["type"] == 'image_url':
524
+ text_content += ("[图片]" if not text_content else '\n[图片]')
525
  elif item["type"] == 'text':
526
+ text_content += (remove_think_tags(item["text"]) if not text_content else '\n' + remove_think_tags(item["text"]))
527
  return text_content
528
  elif isinstance(content, dict) and content is not None:
529
  if content["type"] == 'image_url':
 
531
  elif content["type"] == 'text':
532
  return remove_think_tags(content["text"])
533
  return remove_think_tags(self.process_message_content(content))
534
+
535
  for current in todo_messages:
536
  role = 'assistant' if current["role"] == 'assistant' else 'user'
537
  is_last_message = current == todo_messages[-1]
538
+
 
539
  if is_last_message and "content" in current:
540
  if isinstance(current["content"], list):
541
  for item in current["content"]:
542
+ if item["type"] == 'image_url':
543
+ processed_image = self.upload_base64_image(
544
+ item["image_url"]["url"],
545
+ f"{CONFIG['API']['BASE_URL']}/api/rpc"
546
+ )
547
  if processed_image:
548
  file_attachments.append(processed_image)
549
  elif isinstance(current["content"], dict) and current["content"].get("type") == 'image_url':
550
+ processed_image = self.upload_base64_image(
551
+ current["content"]["image_url"]["url"],
552
+ f"{CONFIG['API']['BASE_URL']}/api/rpc"
553
+ )
554
  if processed_image:
555
  file_attachments.append(processed_image)
556
+
557
 
558
+ text_content = process_content(current.get("content", ""))
559
+
 
560
  if text_content or (is_last_message and file_attachments):
561
  if role == last_role and text_content:
562
  last_content += '\n' + text_content
 
565
  messages += f"{role.upper()}: {text_content or '[图片]'}\n"
566
  last_content = text_content
567
  last_role = role
568
+
569
  return {
570
+ "temporary": CONFIG["API"].get("IS_TEMP_CONVERSATION", False),
571
  "modelName": self.model_id,
572
  "message": messages.strip(),
573
  "fileAttachments": file_attachments[:4],
 
580
  "imageGenerationCount": 1,
581
  "forceConcise": False,
582
  "toolOverrides": {
583
+ "imageGen": request["model"] in ['grok-2-imageGen', 'grok-3-imageGen'],
584
  "webSearch": search,
585
  "xSearch": search,
586
  "xMediaSearch": search,
 
591
  "isPreset": False,
592
  "sendFinalMetadata": True,
593
  "customInstructions": "",
594
+ "deepsearchPreset": "default" if request["model"] == 'grok-3-deepsearch' else "",
595
+ "isReasoning": request["model"] == 'grok-3-reasoning'
596
  }
597
 
598
  class MessageProcessor:
599
  @staticmethod
600
  def create_chat_response(message, model, is_stream=False):
 
601
  base_response = {
602
+ "id": f"chatcmpl-{uuid.uuid4()}",
603
  "created": int(time.time()),
604
  "model": model
605
  }
606
+
607
  if is_stream:
608
  return {
609
  **base_response,
 
631
  }
632
 
633
  def process_model_response(response, model):
 
634
  result = {"token": None, "imageUrl": None}
635
 
636
  if CONFIG["IS_IMG_GEN"]:
637
+ if response.get("cachedImageGenerationResponse") and not CONFIG["IS_IMG_GEN2"]:
638
  result["imageUrl"] = response["cachedImageGenerationResponse"]["imageUrl"]
639
  return result
640
+
 
641
  if model == 'grok-2':
642
  result["token"] = response.get("token")
643
  elif model in ['grok-2-search', 'grok-3-search']:
644
+ if response.get("webSearchResults") and CONFIG["ISSHOW_SEARCH_RESULTS"]:
645
  result["token"] = f"\r\n<think>{Utils.organize_search_results(response['webSearchResults'])}</think>\r\n"
646
  else:
647
  result["token"] = response.get("token")
648
  elif model == 'grok-3':
649
  result["token"] = response.get("token")
650
  elif model == 'grok-3-deepsearch':
651
+ if response.get("messageStepId") and not CONFIG["SHOW_THINKING"]:
652
+ return result
653
+ if response.get("messageStepId") and not CONFIG["IS_THINKING"]:
654
+ result["token"] = "<think>" + response.get("token", "")
655
+ CONFIG["IS_THINKING"] = True
656
+ elif not response.get("messageStepId") and CONFIG["IS_THINKING"] and response.get("messageTag") == "final":
657
+ result["token"] = "</think>" + response.get("token", "")
658
+ CONFIG["IS_THINKING"] = False
659
+ elif (response.get("messageStepId") and CONFIG["IS_THINKING"] and response.get("messageTag") == "assistant") or response.get("messageTag") == "final":
660
+ result["token"] = response.get("token")
661
  elif model == 'grok-3-reasoning':
662
+ if response.get("isThinking") and not CONFIG["SHOW_THINKING"]:
663
  return result
664
+
665
+ if response.get("isThinking") and not CONFIG["IS_THINKING"]:
666
  result["token"] = "<think>" + response.get("token", "")
667
  CONFIG["IS_THINKING"] = True
668
+ elif not response.get("isThinking") and CONFIG["IS_THINKING"]:
669
  result["token"] = "</think>" + response.get("token", "")
670
  CONFIG["IS_THINKING"] = False
671
  else:
672
  result["token"] = response.get("token")
673
+
674
  return result
675
 
676
+ def handle_image_response(image_url):
677
+ max_retries = 2
678
+ retry_count = 0
679
+ image_base64_response = None
680
+
681
+ while retry_count < max_retries:
682
+ try:
683
+ proxy_options = Utils.get_proxy_options()
684
+ image_base64_response = curl_requests.get(
685
+ f"https://assets.grok.com/{image_url}",
686
+ headers={
687
+ **DEFAULT_HEADERS,
688
+ "Cookie": CONFIG["API"]["SIGNATURE_COOKIE"]
689
+ },
690
+ impersonate="chrome120",
691
+ **proxy_options
692
+ )
693
+
694
+ if image_base64_response.status_code == 200:
695
+ break
696
+
697
+ retry_count += 1
698
+ if retry_count == max_retries:
699
+ raise Exception(f"上游服务请求失败! status: {image_base64_response.status_code}")
700
+
701
+ time.sleep(CONFIG["API"]["RETRY_TIME"] / 1000 * retry_count)
702
 
703
+ except Exception as error:
704
+ logger.error(str(error), "Server")
705
+ retry_count += 1
706
+ if retry_count == max_retries:
707
+ raise
708
+
709
+ time.sleep(CONFIG["API"]["RETRY_TIME"] / 1000 * retry_count)
710
+
711
+ image_buffer = image_base64_response.content
712
+
713
+ if not CONFIG["API"]["PICGO_KEY"] and not CONFIG["API"]["TUMY_KEY"]:
714
+ base64_image = base64.b64encode(image_buffer).decode('utf-8')
715
+ image_content_type = image_base64_response.headers.get('content-type', 'image/jpeg')
716
+ return f"![image](data:{image_content_type};base64,{base64_image})"
717
+
718
+ logger.info("开始上传图床", "Server")
719
+
720
+ if CONFIG["API"]["PICGO_KEY"]:
721
+ files = {'source': ('image.jpg', image_buffer, 'image/jpeg')}
722
+ headers = {
723
+ "X-API-Key": CONFIG["API"]["PICGO_KEY"]
724
+ }
725
+
726
+ response_url = requests.post(
727
+ "https://www.picgo.net/api/1/upload",
728
+ files=files,
729
+ headers=headers
730
+ )
731
+
732
+ if response_url.status_code != 200:
733
+ return "生图失败,请查看PICGO图床密钥是否设置正确"
734
+ else:
735
+ logger.info("生图成功", "Server")
736
+ result = response_url.json()
737
+ return f"![image]({result['image']['url']})"
738
+
739
 
740
+ elif CONFIG["API"]["TUMY_KEY"]:
741
+ files = {'file': ('image.jpg', image_buffer, 'image/jpeg')}
742
+ headers = {
743
+ "Accept": "application/json",
744
+ 'Authorization': f"Bearer {CONFIG['API']['TUMY_KEY']}"
745
+ }
746
+
747
+ response_url = requests.post(
748
+ "https://tu.my/api/v1/upload",
749
+ files=files,
750
+ headers=headers
751
+ )
752
+
753
+ if response_url.status_code != 200:
754
+ return "生图失败,请查看TUMY图床密钥是否设置正确"
755
+ else:
756
+ try:
757
+ result = response_url.json()
758
+ logger.info("生图成功", "Server")
759
+ return f"![image]({result['data']['links']['url']})"
760
+ except Exception as error:
761
+ logger.error(str(error), "Server")
762
+ return "生图失败,请查看TUMY图床密钥是否设置正确"
 
 
 
 
 
 
 
 
 
 
763
 
 
 
 
 
764
 
765
+ def handle_non_stream_response(response, model):
 
766
  try:
767
+ content = response.text
768
+ lines = content.split('\n')
769
  full_response = ""
770
+
771
  CONFIG["IS_THINKING"] = False
772
  CONFIG["IS_IMG_GEN"] = False
773
  CONFIG["IS_IMG_GEN2"] = False
774
+
775
+ logger.info("开始处理非流式响应", "Server")
776
+
777
+ for line in lines:
778
+ if not line.strip():
779
  continue
780
+
781
  try:
782
+ line_json = json.loads(line.strip())
783
+ if line_json.get("error"):
784
+ logger.error(json.dumps(line_json, indent=2), "Server")
785
+ if line_json.get("error", {}).get("name") == "RateLimitError":
786
+ CONFIG["API"]["TEMP_COOKIE"] = None
787
  raise ValueError("RateLimitError")
788
+
789
  response_data = line_json.get("result", {}).get("response")
790
  if not response_data:
791
  continue
792
+
793
  if response_data.get("doImgGen") or response_data.get("imageAttachmentInfo"):
794
  CONFIG["IS_IMG_GEN"] = True
795
+
796
  result = process_model_response(response_data, model)
797
 
798
  if result["token"]:
799
  full_response += result["token"]
800
+
801
  if result["imageUrl"]:
802
  CONFIG["IS_IMG_GEN2"] = True
803
+ return handle_image_response(result["imageUrl"])
804
+
805
+ except json.JSONDecodeError:
 
 
806
  continue
807
+ except Exception as e:
808
+ logger.error(f"处理响应行时出错: {str(e)}", "Server")
809
+ continue
810
+
811
+ return full_response
812
+
813
  except Exception as error:
814
+ logger.error(str(error), "Server")
815
+ raise
816
 
817
+ def handle_stream_response(response, model):
818
+ def generate():
819
+ stream = response.iter_lines()
 
 
 
 
 
 
 
 
 
 
 
 
820
 
821
+ CONFIG["IS_THINKING"] = False
822
+ CONFIG["IS_IMG_GEN"] = False
823
+ CONFIG["IS_IMG_GEN2"] = False
824
+ logger.info("开始处理流式响应", "Server")
825
+
826
+ for chunk in stream:
827
+ if not chunk:
828
+ continue
829
+ try:
830
+ line_json = json.loads(chunk.decode("utf-8").strip())
831
+ if line_json.get("error"):
832
+ logger.error(json.dumps(line_json, indent=2), "Server")
833
+ yield json.dumps({"error": "RateLimitError"}) + "\n\n"
834
+ return
835
 
836
+ response_data = line_json.get("result", {}).get("response")
837
+ if not response_data:
838
+ continue
839
 
840
+ if response_data.get("doImgGen") or response_data.get("imageAttachmentInfo"):
841
+ CONFIG["IS_IMG_GEN"] = True
842
+
843
+ result = process_model_response(response_data, model)
844
+
845
+ if result["token"]:
846
+ yield f"data: {json.dumps(MessageProcessor.create_chat_response(result['token'], model, True))}\n\n"
847
 
848
+ if result["imageUrl"]:
849
+ CONFIG["IS_IMG_GEN2"] = True
850
+ image_data = handle_image_response(result["imageUrl"])
851
+ yield f"data: {json.dumps(MessageProcessor.create_chat_response(image_data, model, True))}\n\n"
852
+
853
+ except json.JSONDecodeError:
854
+ continue
855
+ except Exception as e:
856
+ logger.error(f"处理流式响应行时出错: {str(e)}", "Server")
857
+ continue
858
+
859
+ yield "data: [DONE]\n\n"
860
+ return generate()
861
 
862
+ def initialization():
863
+ sso_array = os.environ.get("SSO", "").split(',')
864
+ logger.info("开始加载令牌", "Server")
865
+ for sso in sso_array:
866
+ if sso:
867
+ token_manager.add_token(f"sso-rw={sso};sso={sso}")
868
 
869
+ logger.info(f"成功加载令牌: {json.dumps(token_manager.get_all_tokens(), indent=2)}", "Server")
870
+ logger.info(f"令牌加载完成,共加载: {len(token_manager.get_all_tokens())}个令牌", "Server")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
871
 
872
+ if CONFIG["API"]["PROXY"]:
873
+ logger.info(f"代理已设置: {CONFIG['API']['PROXY']}", "Server")
874
+
875
+ logger.info("初始化完成", "Server")
876
+
877
 
 
878
  app = Flask(__name__)
879
+ app.wsgi_app = ProxyFix(app.wsgi_app)
880
+
881
 
882
  @app.before_request
883
  def log_request_info():
884
+ logger.info(f"{request.method} {request.path}", "Request")
885
+
886
+ @app.route('/get/tokens', methods=['GET'])
887
+ def get_tokens():
888
+ auth_token = request.headers.get('Authorization', '').replace('Bearer ', '')
889
+ if CONFIG["API"]["IS_CUSTOM_SSO"]:
890
+ return jsonify({"error": '自定义的SSO令牌模式无法获取轮询sso令牌状态'}), 403
891
+ elif auth_token != CONFIG["API"]["API_KEY"]:
892
+ return jsonify({"error": 'Unauthorized'}), 401
893
+
894
+ return jsonify(token_manager.get_token_status_map())
895
+
896
+ @app.route('/add/token', methods=['POST'])
897
+ def add_token():
898
+ auth_token = request.headers.get('Authorization', '').replace('Bearer ', '')
899
+ if CONFIG["API"]["IS_CUSTOM_SSO"]:
900
+ return jsonify({"error": '自定义的SSO令牌模式无法添加sso令牌'}), 403
901
+ elif auth_token != CONFIG["API"]["API_KEY"]:
902
+ return jsonify({"error": 'Unauthorized'}), 401
903
+
904
+ try:
905
+ sso = request.json.get('sso')
906
+ token_manager.add_token(f"sso-rw={sso};sso={sso}")
907
+ return jsonify(token_manager.get_token_status_map().get(sso, {})), 200
908
+ except Exception as error:
909
+ logger.error(str(error), "Server")
910
+ return jsonify({"error": '添加sso令牌失败'}), 500
911
+
912
+ @app.route('/delete/token', methods=['POST'])
913
+ def delete_token():
914
+ auth_token = request.headers.get('Authorization', '').replace('Bearer ', '')
915
+ if CONFIG["API"]["IS_CUSTOM_SSO"]:
916
+ return jsonify({"error": '自定义的SSO令牌模式无法删除sso令牌'}), 403
917
+ elif auth_token != CONFIG["API"]["API_KEY"]:
918
+ return jsonify({"error": 'Unauthorized'}), 401
919
+
920
+ try:
921
+ sso = request.json.get('sso')
922
+ token_manager.delete_token(f"sso-rw={sso};sso={sso}")
923
+ return jsonify({"message": '删除sso令牌成功'}), 200
924
+ except Exception as error:
925
+ logger.error(str(error), "Server")
926
+ return jsonify({"error": '删除sso令牌失败'}), 500
927
 
928
  @app.route('/v1/models', methods=['GET'])
929
+ def get_models():
 
930
  return jsonify({
931
  "object": "list",
932
  "data": [
 
935
  "object": "model",
936
  "created": int(time.time()),
937
  "owned_by": "grok"
938
+ }
939
+ for model in token_manager.token_model_map.keys()
940
  ]
941
  })
942
 
943
  @app.route('/v1/chat/completions', methods=['POST'])
944
+ def chat_completions():
 
945
  try:
 
946
  auth_token = request.headers.get('Authorization', '').replace('Bearer ', '')
947
+ if auth_token:
948
+ if CONFIG["API"]["IS_CUSTOM_SSO"]:
949
+ result = f"sso={auth_token};sso-rw={auth_token}"
950
+ token_manager.set_token(result)
951
+ elif auth_token != CONFIG["API"]["API_KEY"]:
952
+ return jsonify({"error": 'Unauthorized'}), 401
953
+ else:
954
+ return jsonify({"error": 'API_KEY缺失'}), 401
955
+
956
+ data = request.json
957
  model = data.get("model")
958
  stream = data.get("stream", False)
959
 
960
+ retry_count = 0
961
  grok_client = GrokApiClient(model)
962
+ request_payload = grok_client.prepare_chat_request(data)
 
963
 
 
 
964
  while retry_count < CONFIG["RETRY"]["MAX_ATTEMPTS"]:
965
  retry_count += 1
966
+ CONFIG["API"]["SIGNATURE_COOKIE"] = Utils.create_auth_headers(model)
967
 
968
+ if not CONFIG["API"]["SIGNATURE_COOKIE"]:
969
+ raise ValueError('该模型无可用令牌')
970
 
971
+ logger.info(f"当前令牌: {json.dumps(CONFIG['API']['SIGNATURE_COOKIE'], indent=2)}", "Server")
972
+ logger.info(f"当前可用模型的全部可用数量: {json.dumps(token_manager.get_remaining_token_request_capacity(), indent=2)}", "Server")
 
 
 
 
 
 
 
 
 
 
 
 
973
 
974
+ try:
975
+ proxy_options = Utils.get_proxy_options()
976
+ response = curl_requests.post(
977
+ f"{CONFIG['API']['BASE_URL']}/rest/app-chat/conversations/new",
978
+ headers={
979
+ "Accept": "text/event-stream",
980
+ "Baggage": "sentry-public_key=b311e0f2690c81f25e2c4cf6d4f7ce1c",
981
+ "Content-Type": "text/plain;charset=UTF-8",
982
+ "Connection": "keep-alive",
983
+ "Cookie": CONFIG["API"]["SIGNATURE_COOKIE"]
984
+ },
985
+ data=json.dumps(request_payload),
986
+ impersonate="chrome120",
987
+ stream=True,
988
+ **proxy_options
989
+ )
990
+
991
+ if response.status_code == 200:
992
+ logger.info("请求成功", "Server")
993
+ logger.info(f"当前{model}剩余可用令牌数: {token_manager.get_token_count_for_model(model)}", "Server")
994
+
995
+ try:
996
+ if stream:
997
+ return Response(
998
+ stream_with_context(handle_stream_response(response, model)),
999
+ content_type='text/event-stream'
1000
+ )
1001
+ else:
1002
+ content = handle_non_stream_response(response, model)
1003
+ return jsonify(MessageProcessor.create_chat_response(content, model))
1004
+
1005
+ except Exception as error:
1006
+ logger.error(str(error), "Server")
1007
+ if CONFIG["API"]["IS_CUSTOM_SSO"]:
1008
+ raise ValueError(f"自定义SSO令牌当前模型{model}的请求次数已失效")
1009
+
1010
+ token_manager.remove_token_from_model(model, CONFIG["API"]["SIGNATURE_COOKIE"])
1011
+ if token_manager.get_token_count_for_model(model) == 0:
1012
+ raise ValueError(f"{model} 次数已达上限,请切换其他模型或者重新对话")
1013
+
1014
+ elif response.status_code == 429:
1015
+ if CONFIG["API"]["IS_CUSTOM_SSO"]:
1016
+ raise ValueError(f"自定义SSO令牌当前模型{model}的请求次数已失效")
1017
+
1018
+ token_manager.remove_token_from_model(model, CONFIG["API"]["SIGNATURE_COOKIE"])
1019
+ if token_manager.get_token_count_for_model(model) == 0:
1020
+ raise ValueError(f"{model} 次数已达上限,请切换其他模型或者重新对话")
1021
 
 
 
 
 
 
1022
  else:
1023
+ if CONFIG["API"]["IS_CUSTOM_SSO"]:
1024
+ raise ValueError(f"自定义SSO令牌当前模型{model}的请求次数已失效")
 
 
 
1025
 
1026
+ logger.error(f"令牌异常错误状态!status: {response.status_code}", "Server")
1027
+ token_manager.remove_token_from_model(model, CONFIG["API"]["SIGNATURE_COOKIE"])
1028
+ logger.info(f"当前{model}剩余可用令牌数: {token_manager.get_token_count_for_model(model)}", "Server")
 
 
 
 
 
 
 
1029
 
1030
+ except Exception as e:
1031
+ logger.error(f"请求处理异常: {str(e)}", "Server")
1032
+ if CONFIG["API"]["IS_CUSTOM_SSO"]:
1033
+ raise
1034
+ continue
1035
+
1036
+ raise ValueError('当前模型所有令牌都已耗尽')
1037
+
1038
  except Exception as error:
1039
+ logger.error(str(error), "ChatAPI")
1040
  return jsonify({
1041
  "error": {
1042
  "message": str(error),
 
1047
  @app.route('/', defaults={'path': ''})
1048
  @app.route('/<path:path>')
1049
  def catch_all(path):
1050
+ return 'api运行正常', 200
1051
+
1052
+ if __name__ == '__main__':
1053
+ token_manager = AuthTokenManager()
1054
+ initialization()
1055
+
1056
+ app.run(
1057
+ host='0.0.0.0',
1058
+ port=CONFIG["SERVER"]["PORT"],
1059
+ debug=False
1060
+ )