merge: remove unused header, retry, and Statsig utility files
This commit is contained in:
1 parent
a864751543
commit
90fc016807
4 files changed
+13
-348
No files matched your search
@@ -1,31 +0,0 @@
|
||||
"""
|
||||
Common header helpers for Grok services.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from typing import Dict
|
||||
|
||||
from app.core.config import get_config
|
||||
from app.services.grok.utils.statsig import StatsigService
|
||||
|
||||
|
||||
def _normalize_token(token: str) -> str:
|
||||
return token[4:] if token.startswith("sso=") else token
|
||||
|
||||
|
||||
def build_sso_cookie(token: str, include_rw: bool = False) -> str:
|
||||
token = _normalize_token(token)
|
||||
cf = get_config("security.cf_clearance")
|
||||
cookie = f"sso={token}"
|
||||
if include_rw:
|
||||
cookie = f"{cookie}; sso-rw={token}"
|
||||
if cf:
|
||||
cookie = f"{cookie};cf_clearance={cf}"
|
||||
return cookie
|
||||
|
||||
|
||||
def apply_statsig(headers: Dict[str, str]) -> None:
|
||||
headers["x-statsig-id"] = StatsigService.gen_id()
|
||||
headers["x-xai-request-id"] = str(uuid.uuid4())
|
||||
@@ -1,264 +0,0 @@
|
||||
"""
|
||||
Grok API 重试工具
|
||||
|
||||
提供可配置的重试机制,支持:
|
||||
- 指数退避 + decorrelated jitter
|
||||
- Retry-After header 支持
|
||||
- 429 专用退避策略
|
||||
- 重试预算控制
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import random
|
||||
from typing import Callable, Any, Optional
|
||||
from functools import wraps
|
||||
|
||||
from app.core.logger import logger
|
||||
from app.core.config import get_config
|
||||
from app.core.exceptions import UpstreamException
|
||||
|
||||
|
||||
class RetryContext:
|
||||
"""重试上下文"""
|
||||
|
||||
def __init__(self):
|
||||
self.attempt = 0
|
||||
self.max_retry = int(get_config("retry.max_retry"))
|
||||
self.retry_codes = get_config("retry.retry_status_codes")
|
||||
self.last_error = None
|
||||
self.last_status = None
|
||||
self.total_delay = 0.0
|
||||
self.retry_budget = float(get_config("retry.retry_budget"))
|
||||
|
||||
# 退避参数
|
||||
self.backoff_base = float(get_config("retry.retry_backoff_base"))
|
||||
self.backoff_factor = float(get_config("retry.retry_backoff_factor"))
|
||||
self.backoff_max = float(get_config("retry.retry_backoff_max"))
|
||||
|
||||
# decorrelated jitter 状态
|
||||
self._last_delay = self.backoff_base
|
||||
|
||||
def should_retry(self, status_code: int) -> bool:
|
||||
"""判断是否重试"""
|
||||
if self.attempt >= self.max_retry:
|
||||
return False
|
||||
if status_code not in self.retry_codes:
|
||||
return False
|
||||
if self.total_delay >= self.retry_budget:
|
||||
return False
|
||||
return True
|
||||
|
||||
def record_error(self, status_code: int, error: Exception):
|
||||
"""记录错误信息"""
|
||||
self.last_status = status_code
|
||||
self.last_error = error
|
||||
self.attempt += 1
|
||||
|
||||
def calculate_delay(
|
||||
self, status_code: int, retry_after: Optional[float] = None
|
||||
) -> float:
|
||||
"""
|
||||
计算退避延迟时间
|
||||
|
||||
Args:
|
||||
status_code: HTTP 状态码
|
||||
retry_after: Retry-After header 值(秒)
|
||||
|
||||
Returns:
|
||||
延迟时间(秒)
|
||||
"""
|
||||
# 优先使用 Retry-After
|
||||
if retry_after is not None and retry_after > 0:
|
||||
delay = min(retry_after, self.backoff_max)
|
||||
self._last_delay = delay
|
||||
return delay
|
||||
|
||||
# 429 使用 decorrelated jitter
|
||||
if status_code == 429:
|
||||
# decorrelated jitter: delay = random(base, last_delay * 3)
|
||||
delay = random.uniform(self.backoff_base, self._last_delay * 3)
|
||||
delay = min(delay, self.backoff_max)
|
||||
self._last_delay = delay
|
||||
return delay
|
||||
|
||||
# 其他状态码使用指数退避 + full jitter
|
||||
exp_delay = self.backoff_base * (self.backoff_factor**self.attempt)
|
||||
delay = random.uniform(0, min(exp_delay, self.backoff_max))
|
||||
return delay
|
||||
|
||||
def record_delay(self, delay: float):
|
||||
"""记录延迟时间"""
|
||||
self.total_delay += delay
|
||||
|
||||
|
||||
def extract_retry_after(error: Exception) -> Optional[float]:
|
||||
"""
|
||||
从异常中提取 Retry-After 值
|
||||
|
||||
Args:
|
||||
error: 异常对象
|
||||
|
||||
Returns:
|
||||
Retry-After 秒数,或 None
|
||||
"""
|
||||
if not isinstance(error, UpstreamException):
|
||||
return None
|
||||
|
||||
details = error.details or {}
|
||||
|
||||
# 尝试从 details 中获取
|
||||
retry_after = details.get("retry_after")
|
||||
if retry_after is not None:
|
||||
try:
|
||||
return float(retry_after)
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
# 尝试从 headers 中获取
|
||||
headers = details.get("headers", {})
|
||||
if isinstance(headers, dict):
|
||||
retry_after = headers.get("Retry-After") or headers.get("retry-after")
|
||||
if retry_after is not None:
|
||||
try:
|
||||
return float(retry_after)
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
|
||||
async def retry_on_status(
|
||||
func: Callable,
|
||||
*args,
|
||||
extract_status: Callable[[Exception], Optional[int]] = None,
|
||||
on_retry: Callable[[int, int, Exception, float], None] = None,
|
||||
**kwargs,
|
||||
) -> Any:
|
||||
"""
|
||||
通用重试函数
|
||||
|
||||
Args:
|
||||
func: 重试的异步函数
|
||||
*args: 函数参数
|
||||
extract_status: 异常提取状态码的函数
|
||||
on_retry: 重试时的回调函数 (attempt, status_code, error, delay)
|
||||
**kwargs: 函数关键字参数
|
||||
|
||||
Returns:
|
||||
函数执行结果
|
||||
|
||||
Raises:
|
||||
最后一次失败的异常
|
||||
"""
|
||||
ctx = RetryContext()
|
||||
|
||||
# 状态码提取器
|
||||
if extract_status is None:
|
||||
|
||||
def extract_status(e: Exception) -> Optional[int]:
|
||||
if isinstance(e, UpstreamException):
|
||||
# 优先从 details 获取,回退到 status_code 属性
|
||||
if e.details and "status" in e.details:
|
||||
return e.details["status"]
|
||||
return getattr(e, "status_code", None)
|
||||
return None
|
||||
|
||||
while ctx.attempt <= ctx.max_retry:
|
||||
try:
|
||||
result = await func(*args, **kwargs)
|
||||
|
||||
# 记录日志
|
||||
if ctx.attempt > 0:
|
||||
logger.info(
|
||||
f"Retry succeeded after {ctx.attempt} attempts, "
|
||||
f"total delay: {ctx.total_delay:.2f}s"
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
# 提取状态码
|
||||
status_code = extract_status(e)
|
||||
|
||||
if status_code is None:
|
||||
# 错误无法识别
|
||||
logger.error(f"Non-retryable error: {e}")
|
||||
raise
|
||||
|
||||
# 记录错误
|
||||
ctx.record_error(status_code, e)
|
||||
|
||||
# 判断是否重试
|
||||
if ctx.should_retry(status_code):
|
||||
# 提取 Retry-After
|
||||
retry_after = extract_retry_after(e)
|
||||
|
||||
# 计算延迟
|
||||
delay = ctx.calculate_delay(status_code, retry_after)
|
||||
|
||||
# 检查是否超出预算
|
||||
if ctx.total_delay + delay > ctx.retry_budget:
|
||||
logger.warning(
|
||||
f"Retry budget exhausted: {ctx.total_delay:.2f}s + {delay:.2f}s > {ctx.retry_budget}s"
|
||||
)
|
||||
raise
|
||||
|
||||
ctx.record_delay(delay)
|
||||
|
||||
logger.warning(
|
||||
f"Retry {ctx.attempt}/{ctx.max_retry} for status {status_code}, "
|
||||
f"waiting {delay:.2f}s (total: {ctx.total_delay:.2f}s)"
|
||||
+ (f", Retry-After: {retry_after}s" if retry_after else "")
|
||||
)
|
||||
|
||||
# 回调
|
||||
if on_retry:
|
||||
on_retry(ctx.attempt, status_code, e, delay)
|
||||
|
||||
await asyncio.sleep(delay)
|
||||
continue
|
||||
else:
|
||||
# 不可重试或重试次数耗尽
|
||||
if status_code in ctx.retry_codes:
|
||||
logger.error(
|
||||
f"Retry exhausted after {ctx.attempt} attempts, "
|
||||
f"last status: {status_code}, total delay: {ctx.total_delay:.2f}s"
|
||||
)
|
||||
else:
|
||||
logger.error(f"Non-retryable status code: {status_code}")
|
||||
|
||||
# 抛出最后一次的错误
|
||||
raise
|
||||
|
||||
|
||||
def with_retry(
|
||||
extract_status: Callable[[Exception], Optional[int]] = None,
|
||||
on_retry: Callable[[int, int, Exception, float], None] = None,
|
||||
):
|
||||
"""
|
||||
重试装饰器
|
||||
|
||||
Usage:
|
||||
@with_retry()
|
||||
async def my_api_call():
|
||||
...
|
||||
"""
|
||||
|
||||
def decorator(func: Callable):
|
||||
@wraps(func)
|
||||
async def wrapper(*args, **kwargs):
|
||||
return await retry_on_status(
|
||||
func, *args, extract_status=extract_status, on_retry=on_retry, **kwargs
|
||||
)
|
||||
|
||||
return wrapper
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
__all__ = [
|
||||
"RetryContext",
|
||||
"retry_on_status",
|
||||
"with_retry",
|
||||
"extract_retry_after",
|
||||
]
|
||||
@@ -1,51 +0,0 @@
|
||||
"""
|
||||
Statsig ID 生成服务
|
||||
"""
|
||||
|
||||
import base64
|
||||
import random
|
||||
import string
|
||||
|
||||
from app.core.config import get_config
|
||||
|
||||
|
||||
class StatsigService:
|
||||
"""Statsig ID 生成服务"""
|
||||
|
||||
@staticmethod
|
||||
def _rand(length: int, alphanumeric: bool = False) -> str:
|
||||
"""生成随机字符串"""
|
||||
chars = (
|
||||
string.ascii_lowercase + string.digits
|
||||
if alphanumeric
|
||||
else string.ascii_lowercase
|
||||
)
|
||||
return "".join(random.choices(chars, k=length))
|
||||
|
||||
@staticmethod
|
||||
def gen_id() -> str:
|
||||
"""
|
||||
生成 Statsig ID
|
||||
|
||||
Returns:
|
||||
Base64 编码的 ID
|
||||
"""
|
||||
dynamic = get_config("chat.dynamic_statsig")
|
||||
|
||||
if not dynamic:
|
||||
return "ZTpUeXBlRXJyb3I6IENhbm5vdCByZWFkIHByb3BlcnRpZXMgb2YgdW5kZWZpbmVkIChyZWFkaW5nICdjaGlsZE5vZGVzJyk="
|
||||
|
||||
# 随机格式
|
||||
if random.choice([True, False]):
|
||||
rand = StatsigService._rand(5, alphanumeric=True)
|
||||
message = f"e:TypeError: Cannot read properties of null (reading 'children['{rand}']')"
|
||||
else:
|
||||
rand = StatsigService._rand(10)
|
||||
message = (
|
||||
f"e:TypeError: Cannot read properties of undefined (reading '{rand}')"
|
||||
)
|
||||
|
||||
return base64.b64encode(message.encode()).decode()
|
||||
|
||||
|
||||
__all__ = ["StatsigService"]
|
||||
@@ -3,7 +3,7 @@ Reverse interface: app chat conversations.
|
||||
"""
|
||||
|
||||
import orjson
|
||||
from typing import Any, Dict, List
|
||||
from typing import Any, Dict, List, Optional
|
||||
from curl_cffi.requests import AsyncSession
|
||||
|
||||
from app.core.logger import logger
|
||||
@@ -165,7 +165,18 @@ class AppChatReverse:
|
||||
|
||||
return response
|
||||
|
||||
response = await retry_on_status(_do_request)
|
||||
def extract_status(e: Exception) -> Optional[int]:
|
||||
if isinstance(e, UpstreamException):
|
||||
if e.details and "status" in e.details:
|
||||
status = e.details["status"]
|
||||
else:
|
||||
status = getattr(e, "status_code", None)
|
||||
if status == 429:
|
||||
return None
|
||||
return status
|
||||
return None
|
||||
|
||||
response = await retry_on_status(_do_request, extract_status=extract_status)
|
||||
|
||||
# Stream response
|
||||
async def stream_response():
|
||||
|
||||
Reference in new issue
Block a user