├── .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 | 
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""
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""
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""
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 |
--------------------------------------------------------------------------------