Files
grok2api/app/services/grok/client.py
T
2025-12-17 00:43:44 +08:00

316 lines
13 KiB
Python

"""Grok API 客户端 - 处理OpenAI到Grok的请求转换和响应处理"""
import asyncio
import orjson
from typing import Dict, List, Tuple, Any, Optional
from curl_cffi import requests as curl_requests
from app.core.config import setting
from app.core.logger import logger
from app.models.grok_models import Models
from app.services.grok.processer import GrokResponseProcessor
from app.services.grok.statsig import get_dynamic_headers
from app.services.grok.token import token_manager
from app.services.grok.upload import ImageUploadManager
from app.services.grok.create import PostCreateManager
from app.core.exception import GrokApiException
# 常量
API_ENDPOINT = "https://grok.com/rest/app-chat/conversations/new"
TIMEOUT = 120
BROWSER = "chrome133a"
MAX_RETRY = 3
MAX_UPLOADS = 20 # 提高并发上传限制以支持更高并发
class GrokClient:
"""Grok API 客户端"""
_upload_sem = None # 延迟初始化
@staticmethod
def _get_upload_semaphore():
"""获取上传信号量(动态配置)"""
if GrokClient._upload_sem is None:
# 从配置读取,如果不可用则使用默认值
max_concurrency = setting.global_config.get("max_upload_concurrency", MAX_UPLOADS)
GrokClient._upload_sem = asyncio.Semaphore(max_concurrency)
logger.debug(f"[Client] 初始化上传并发限制: {max_concurrency}")
return GrokClient._upload_sem
@staticmethod
async def openai_to_grok(request: dict):
"""转换OpenAI请求为Grok请求"""
model = request["model"]
content, images = GrokClient._extract_content(request["messages"])
stream = request.get("stream", False)
# 获取模型信息
info = Models.get_model_info(model)
grok_model, mode = Models.to_grok(model)
is_video = info.get("is_video_model", False)
# 视频模型限制
if is_video and len(images) > 1:
logger.warning(f"[Client] 视频模型仅支持1张图片,已截取前1张")
images = images[:1]
return await GrokClient._retry(model, content, images, grok_model, mode, is_video, stream)
@staticmethod
async def _retry(model: str, content: str, images: List[str], grok_model: str, mode: str, is_video: bool, stream: bool):
"""重试请求"""
last_err = None
for i in range(MAX_RETRY):
try:
token = token_manager.get_token(model)
img_ids, img_uris = await GrokClient._upload(images, token)
# 视频模型创建会话
post_id = None
if is_video and img_ids and img_uris:
post_id = await GrokClient._create_post(img_ids[0], img_uris[0], token)
payload = GrokClient._build_payload(content, grok_model, mode, img_ids, img_uris, is_video, post_id)
return await GrokClient._request(payload, token, model, stream, post_id)
except GrokApiException as e:
last_err = e
# 检查是否可重试
if e.error_code not in ["HTTP_ERROR", "NO_AVAILABLE_TOKEN"]:
raise
status = e.context.get("status") if e.context else None
retry_codes = setting.grok_config.get("retry_status_codes", [401, 429])
if status not in retry_codes:
raise
if i < MAX_RETRY - 1:
logger.warning(f"[Client] 失败(状态:{status}), 重试 {i+1}/{MAX_RETRY}")
await asyncio.sleep(0.5)
raise last_err or GrokApiException("请求失败", "REQUEST_ERROR")
@staticmethod
def _extract_content(messages: List[Dict]) -> Tuple[str, List[str]]:
"""提取文本和图片"""
texts, images = [], []
for msg in messages:
content = msg.get("content", "")
if isinstance(content, list):
for item in content:
if item.get("type") == "text":
texts.append(item.get("text", ""))
elif item.get("type") == "image_url":
if url := item.get("image_url", {}).get("url"):
images.append(url)
else:
texts.append(content)
return "".join(texts), images
@staticmethod
async def _upload(urls: List[str], token: str) -> Tuple[List[str], List[str]]:
"""并发上传图片"""
if not urls:
return [], []
async def upload_limited(url):
async with GrokClient._get_upload_semaphore():
return await ImageUploadManager.upload(url, token)
results = await asyncio.gather(*[upload_limited(u) for u in urls], return_exceptions=True)
ids, uris = [], []
for url, result in zip(urls, results):
if isinstance(result, Exception):
logger.warning(f"[Client] 上传失败: {url} - {result}")
elif isinstance(result, tuple) and len(result) == 2:
fid, furi = result
if fid:
ids.append(fid)
uris.append(furi)
return ids, uris
@staticmethod
async def _create_post(file_id: str, file_uri: str, token: str) -> Optional[str]:
"""创建视频会话"""
try:
result = await PostCreateManager.create(file_id, file_uri, token)
if result and result.get("success"):
return result.get("post_id")
except Exception as e:
logger.warning(f"[Client] 创建会话失败: {e}")
return None
@staticmethod
def _build_payload(content: str, model: str, mode: str, img_ids: List[str], img_uris: List[str], is_video: bool = False, post_id: str = None) -> Dict:
"""构建请求载荷"""
# 视频模型特殊处理
if is_video and img_uris:
img_msg = f"https://grok.com/imagine/{post_id}" if post_id else f"https://assets.grok.com/post/{img_uris[0]}"
return {
"temporary": True,
"modelName": "grok-3",
"message": f"{img_msg} {content} --mode=custom",
"fileAttachments": img_ids,
"toolOverrides": {"videoGen": True}
}
# 标准载荷
return {
"temporary": setting.grok_config.get("temporary", True),
"modelName": model,
"message": content,
"fileAttachments": img_ids,
"imageAttachments": [],
"disableSearch": False,
"enableImageGeneration": True,
"returnImageBytes": False,
"returnRawGrokInXaiRequest": False,
"enableImageStreaming": True,
"imageGenerationCount": 2,
"forceConcise": False,
"toolOverrides": {},
"enableSideBySide": True,
"sendFinalMetadata": True,
"isReasoning": False,
"webpageUrls": [],
"disableTextFollowUps": True,
"responseMetadata": {"requestModelDetails": {"modelId": model}},
"disableMemory": False,
"forceSideBySide": False,
"modelMode": mode,
"isAsyncChat": False
}
@staticmethod
async def _request(payload: dict, token: str, model: str, stream: bool, post_id: str = None):
"""发送请求"""
if not token:
raise GrokApiException("认证令牌缺失", "NO_AUTH_TOKEN")
# 403重试配置
max_retries = 5
retry_count = 0
while retry_count <= max_retries:
try:
# 构建请求
headers = GrokClient._build_headers(token)
if model == "grok-imagine-0.9":
file_attachments = payload.get("fileAttachments", [])
ref_id = post_id or (file_attachments[0] if file_attachments else "")
if ref_id:
headers["Referer"] = f"https://grok.com/imagine/{ref_id}"
# 异步获取代理
from app.core.proxy_pool import proxy_pool
# 如果是重试且使用代理池,强制刷新代理
if retry_count > 0 and proxy_pool._enabled:
logger.info(f"[Client] 403重试 {retry_count}/{max_retries},刷新代理...")
proxy = await proxy_pool.force_refresh()
else:
proxy = await setting.get_proxy_async("service")
proxies = {"http": proxy, "https": proxy} if proxy else None
# 执行请求
response = await asyncio.to_thread(
curl_requests.post,
API_ENDPOINT,
headers=headers,
data=orjson.dumps(payload),
impersonate=BROWSER,
timeout=TIMEOUT,
stream=True,
proxies=proxies
)
# 内层403重试:仅当有代理池时触发
if response.status_code == 403 and proxy_pool._enabled:
retry_count += 1
if retry_count <= max_retries:
logger.warning(f"[Client] 遇到403错误,正在重试 ({retry_count}/{max_retries})...")
await asyncio.sleep(0.5)
continue
# 内层重试全部失败
logger.error(f"[Client] 403错误,已重试{retry_count-1}次,放弃")
# 检查响应状态
if response.status_code != 200:
GrokClient._handle_error(response, token)
# 成功 - 重置失败计数
asyncio.create_task(token_manager.reset_failure(token))
# 如果是重试成功,记录日志
if retry_count > 0:
logger.info(f"[Client] 重试成功!第{retry_count}次重试后请求成功")
# 处理响应
result = (GrokResponseProcessor.process_stream(response, token) if stream
else await GrokResponseProcessor.process_normal(response, token, model))
asyncio.create_task(GrokClient._update_limits(token, model))
return result
except curl_requests.RequestsError as e:
logger.error(f"[Client] 网络错误: {e}")
raise GrokApiException(f"网络错误: {e}", "NETWORK_ERROR") from e
except GrokApiException:
# 重新抛出GrokApiException(包括403错误)
raise
except Exception as e:
logger.error(f"[Client] 请求错误: {e}")
raise GrokApiException(f"请求错误: {e}", "REQUEST_ERROR") from e
# 理论上不应该到这里,但以防万一
raise GrokApiException("403错误:已达到最大重试次数", "MAX_RETRIES_EXCEEDED")
@staticmethod
def _build_headers(token: str) -> Dict[str, str]:
"""构建请求头"""
headers = get_dynamic_headers("/rest/app-chat/conversations/new")
cf = setting.grok_config.get("cf_clearance", "")
headers["Cookie"] = f"{token};{cf}" if cf else token
return headers
@staticmethod
def _handle_error(response, token: str):
"""处理错误"""
if response.status_code == 403:
msg = "您的IP被拦截,请尝试以下方法之一: 1.更换IP 2.使用代理 3.配置CF值"
data = {"cf_blocked": True, "status": 403}
logger.warning(f"[Client] {msg}")
else:
try:
data = response.json()
msg = str(data)
except:
data = response.text
msg = data[:200] if data else "未知错误"
asyncio.create_task(token_manager.record_failure(token, response.status_code, msg))
raise GrokApiException(
f"请求失败: {response.status_code} - {msg}",
"HTTP_ERROR",
{"status": response.status_code, "data": data}
)
@staticmethod
async def _update_limits(token: str, model: str):
"""更新速率限制"""
try:
await token_manager.check_limits(token, model)
except Exception as e:
logger.error(f"[Client] 更新限制失败: {e}")