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