Merge branch 'chenyme:main' into main
This commit is contained in:
12 files changed
+124
-51
No files matched your search
+3
-3
@@ -1,6 +1,6 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, Query
|
||||
from fastapi.responses import HTMLResponse, StreamingResponse
|
||||
from app.core.auth import verify_api_key, verify_app_key
|
||||
from app.core.auth import verify_api_key, verify_app_key, get_admin_api_key
|
||||
from app.core.config import config, get_config
|
||||
from app.core.batch_tasks import create_task, get_task, expire_task
|
||||
from app.core.storage import get_storage, LocalStorage, RedisStorage, SQLStorage
|
||||
@@ -33,7 +33,7 @@ def _sse_event(payload: dict) -> str:
|
||||
|
||||
|
||||
def _verify_stream_api_key(request: Request) -> None:
|
||||
api_key = get_config("app.api_key", "")
|
||||
api_key = get_admin_api_key()
|
||||
if not api_key:
|
||||
return
|
||||
key = request.query_params.get("api_key")
|
||||
@@ -110,7 +110,7 @@ async def admin_token_page():
|
||||
@router.post("/api/v1/admin/login", dependencies=[Depends(verify_app_key)])
|
||||
async def admin_login_api():
|
||||
"""管理后台登录验证(使用 app_key)"""
|
||||
return {"status": "success", "api_key": get_config("app.api_key", "")}
|
||||
return {"status": "success", "api_key": get_admin_api_key()}
|
||||
|
||||
|
||||
@router.get("/api/v1/admin/config", dependencies=[Depends(verify_api_key)])
|
||||
|
||||
+9
-9
@@ -48,8 +48,8 @@ class VideoConfig(BaseModel):
|
||||
aspect_ratio: Optional[str] = Field(
|
||||
"3:2", description="视频比例: 3:2, 16:9, 1:1 等"
|
||||
)
|
||||
video_length: Optional[int] = Field(6, description="视频时长(秒): 5-15")
|
||||
resolution: Optional[str] = Field("SD", description="视频分辨率: SD, HD")
|
||||
video_length: Optional[int] = Field(6, description="视频时长(秒): 6 或 10")
|
||||
resolution_name: Optional[str] = Field("480p", description="视频分辨率: 480p, 720p")
|
||||
preset: Optional[str] = Field("custom", description="风格预设: fun, normal, spicy")
|
||||
|
||||
@field_validator("aspect_ratio")
|
||||
@@ -68,22 +68,22 @@ class VideoConfig(BaseModel):
|
||||
@classmethod
|
||||
def validate_video_length(cls, v):
|
||||
if v is not None:
|
||||
if v < 5 or v > 15:
|
||||
if v not in (6, 10):
|
||||
raise ValidationException(
|
||||
message="video_length must be between 5 and 15 seconds",
|
||||
message="video_length must be 6 or 10 seconds",
|
||||
param="video_config.video_length",
|
||||
code="invalid_video_length",
|
||||
)
|
||||
return v
|
||||
|
||||
@field_validator("resolution")
|
||||
@field_validator("resolution_name")
|
||||
@classmethod
|
||||
def validate_resolution(cls, v):
|
||||
allowed = ["SD", "HD"]
|
||||
allowed = ["480p", "720p"]
|
||||
if v and v not in allowed:
|
||||
raise ValidationException(
|
||||
message=f"resolution must be one of {allowed}",
|
||||
param="video_config.resolution",
|
||||
message=f"resolution_name must be one of {allowed}",
|
||||
param="video_config.resolution_name",
|
||||
code="invalid_resolution",
|
||||
)
|
||||
return v
|
||||
@@ -272,7 +272,7 @@ async def chat_completions(request: ChatCompletionRequest):
|
||||
thinking=request.thinking,
|
||||
aspect_ratio=v_conf.aspect_ratio,
|
||||
video_length=v_conf.video_length,
|
||||
resolution=v_conf.resolution,
|
||||
resolution=v_conf.resolution_name,
|
||||
preset=v_conf.preset,
|
||||
)
|
||||
else:
|
||||
|
||||
+14
-2
@@ -16,15 +16,27 @@ security = HTTPBearer(
|
||||
)
|
||||
|
||||
|
||||
def get_admin_api_key() -> str:
|
||||
"""
|
||||
获取后台 API Key。
|
||||
|
||||
为空时表示不启用后台接口认证。
|
||||
"""
|
||||
api_key = get_config("app.api_key", "")
|
||||
return api_key or ""
|
||||
|
||||
|
||||
async def verify_api_key(
|
||||
auth: Optional[HTTPAuthorizationCredentials] = Security(security),
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
验证 Bearer Token
|
||||
|
||||
如果 config.toml 中未配置 api_key,则使用默认密钥 "grok2api"。
|
||||
如果 config.toml 中未配置 api_key,则不启用认证。
|
||||
"""
|
||||
api_key = get_config("app.api_key", "grok2api")
|
||||
api_key = get_admin_api_key()
|
||||
if not api_key:
|
||||
return None
|
||||
|
||||
if not auth:
|
||||
raise HTTPException(
|
||||
|
||||
+13
-13
@@ -177,7 +177,7 @@ class VideoService:
|
||||
post_id: str,
|
||||
aspect_ratio: str = "3:2",
|
||||
video_length: int = 6,
|
||||
resolution: str = "SD",
|
||||
resolution_name: str = "480p",
|
||||
preset: str = "normal",
|
||||
) -> dict:
|
||||
"""构建视频生成载荷"""
|
||||
@@ -190,7 +190,12 @@ class VideoService:
|
||||
mode_flag = "--mode=extremely-spicy-or-crazy"
|
||||
|
||||
full_prompt = f"{prompt} {mode_flag}"
|
||||
|
||||
video_gen_config = {
|
||||
"parentPostId": post_id,
|
||||
"aspectRatio": aspect_ratio,
|
||||
"videoLength": video_length,
|
||||
"resolutionName": resolution_name,
|
||||
}
|
||||
return {
|
||||
"temporary": True,
|
||||
"modelName": "grok-3",
|
||||
@@ -201,12 +206,7 @@ class VideoService:
|
||||
"experiments": [],
|
||||
"modelConfigOverride": {
|
||||
"modelMap": {
|
||||
"videoGenModelConfig": {
|
||||
"parentPostId": post_id,
|
||||
"aspectRatio": aspect_ratio,
|
||||
"videoLength": video_length,
|
||||
"videoResolution": resolution,
|
||||
}
|
||||
"videoGenModelConfig": video_gen_config
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -218,7 +218,7 @@ class VideoService:
|
||||
prompt: str,
|
||||
aspect_ratio: str = "3:2",
|
||||
video_length: int = 6,
|
||||
resolution: str = "SD",
|
||||
resolution_name: str = "480p",
|
||||
stream: bool = True,
|
||||
preset: str = "normal",
|
||||
) -> AsyncGenerator[bytes, None]:
|
||||
@@ -230,7 +230,7 @@ class VideoService:
|
||||
prompt: 视频描述
|
||||
aspect_ratio: 宽高比
|
||||
video_length: 视频时长
|
||||
resolution: 分辨率
|
||||
resolution_name: 分辨率
|
||||
stream: 是否流式
|
||||
preset: 预设
|
||||
|
||||
@@ -249,7 +249,7 @@ class VideoService:
|
||||
# Step 2: 建立连接
|
||||
headers = self._build_headers(token)
|
||||
payload = self._build_payload(
|
||||
prompt, post_id, aspect_ratio, video_length, resolution, preset
|
||||
prompt, post_id, aspect_ratio, video_length, resolution_name, preset
|
||||
)
|
||||
|
||||
session = AsyncSession(impersonate=BROWSER)
|
||||
@@ -302,7 +302,7 @@ class VideoService:
|
||||
image_url: str,
|
||||
aspect_ratio: str = "3:2",
|
||||
video_length: int = 6,
|
||||
resolution: str = "SD",
|
||||
resolution: str = "480p",
|
||||
stream: bool = True,
|
||||
preset: str = "normal",
|
||||
) -> AsyncGenerator[bytes, None]:
|
||||
@@ -418,7 +418,7 @@ class VideoService:
|
||||
thinking: str = None,
|
||||
aspect_ratio: str = "3:2",
|
||||
video_length: int = 6,
|
||||
resolution: str = "SD",
|
||||
resolution: str = "480p",
|
||||
preset: str = "normal",
|
||||
):
|
||||
"""
|
||||
|
||||
@@ -85,11 +85,18 @@ class ModelService:
|
||||
display_name="Grok 4 Heavy",
|
||||
),
|
||||
ModelInfo(
|
||||
model_id="grok-4.1",
|
||||
model_id="grok-4.1-fast",
|
||||
grok_model="grok-4-1-thinking-1129",
|
||||
model_mode="MODEL_MODE_AUTO",
|
||||
model_mode="MODEL_MODE_FAST",
|
||||
cost=Cost.LOW,
|
||||
display_name="Grok 4.1",
|
||||
display_name="Grok 4.1 Fast",
|
||||
),
|
||||
ModelInfo(
|
||||
model_id="grok-4.1-expert",
|
||||
grok_model="grok-4-1-thinking-1129",
|
||||
model_mode="MODEL_MODE_EXPERT",
|
||||
cost=Cost.LOW,
|
||||
display_name="Grok 4.1 Expert",
|
||||
),
|
||||
ModelInfo(
|
||||
model_id="grok-4.1-thinking",
|
||||
|
||||
@@ -62,14 +62,24 @@ class NSFWService:
|
||||
@staticmethod
|
||||
def _build_payload() -> bytes:
|
||||
"""构造请求 payload"""
|
||||
# protobuf: enable_unhinged=true (field1=1, field2=1)
|
||||
protobuf = bytes([0x08, 0x01, 0x10, 0x01])
|
||||
# protobuf (match captured HAR):
|
||||
# 0a 02 10 01 -> field 1 (len=2) with inner bool=true
|
||||
# 12 1a -> field 2, length 26
|
||||
# 0a 18 <name> -> nested message with name string
|
||||
name = b"always_show_nsfw_content"
|
||||
inner = b"\x0a" + bytes([len(name)]) + name
|
||||
protobuf = b"\x0a\x02\x10\x01\x12" + bytes([len(inner)]) + inner
|
||||
return encode_grpc_web_payload(protobuf)
|
||||
|
||||
async def enable(self, token: str) -> NSFWResult:
|
||||
"""为单个 token 开启 NSFW 模式"""
|
||||
headers = self._build_headers(token)
|
||||
payload = self._build_payload()
|
||||
logger.debug(
|
||||
"NSFW payload: len={} hex={}",
|
||||
len(payload),
|
||||
payload.hex(),
|
||||
)
|
||||
proxies = {"http": self.proxy, "https": self.proxy} if self.proxy else None
|
||||
|
||||
try:
|
||||
@@ -96,6 +106,13 @@ class NSFWService:
|
||||
)
|
||||
|
||||
grpc_status = get_grpc_status(trailers)
|
||||
logger.debug(
|
||||
"NSFW response: http={} grpc={} msg={} trailers={}",
|
||||
response.status_code,
|
||||
grpc_status.code,
|
||||
grpc_status.message,
|
||||
trailers,
|
||||
)
|
||||
|
||||
# HTTP 200 且无 grpc-status(空响应)或 grpc-status=0 都算成功
|
||||
success = grpc_status.code == -1 or grpc_status.ok
|
||||
|
||||
@@ -498,7 +498,7 @@ class VideoStreamProcessor(BaseProcessor):
|
||||
self.response_id: Optional[str] = None
|
||||
self.think_opened: bool = False
|
||||
self.role_sent: bool = False
|
||||
self.video_format = get_config("app.video_format", "url")
|
||||
self.video_format = str(get_config("app.video_format", "html")).lower()
|
||||
|
||||
if think is None:
|
||||
self.show_think = get_config("grok.thinking", False)
|
||||
@@ -568,10 +568,13 @@ class VideoStreamProcessor(BaseProcessor):
|
||||
thumbnail_url, "image"
|
||||
)
|
||||
|
||||
video_html = self._build_video_html(
|
||||
final_video_url, final_thumbnail_url
|
||||
)
|
||||
yield self._sse(video_html)
|
||||
if self.video_format == "url":
|
||||
yield self._sse(final_video_url)
|
||||
else:
|
||||
video_html = self._build_video_html(
|
||||
final_video_url, final_thumbnail_url
|
||||
)
|
||||
yield self._sse(video_html)
|
||||
|
||||
logger.info(f"Video generated: {video_url}")
|
||||
continue
|
||||
@@ -626,7 +629,7 @@ class VideoCollectProcessor(BaseProcessor):
|
||||
|
||||
def __init__(self, model: str, token: str = ""):
|
||||
super().__init__(model, token)
|
||||
self.video_format = get_config("app.video_format", "url")
|
||||
self.video_format = str(get_config("app.video_format", "html")).lower()
|
||||
|
||||
def _build_video_html(self, video_url: str, thumbnail_url: str = "") -> str:
|
||||
poster_attr = f' poster="{thumbnail_url}"' if thumbnail_url else ""
|
||||
@@ -666,9 +669,12 @@ class VideoCollectProcessor(BaseProcessor):
|
||||
thumbnail_url, "image"
|
||||
)
|
||||
|
||||
content = self._build_video_html(
|
||||
final_video_url, final_thumbnail_url
|
||||
)
|
||||
if self.video_format == "url":
|
||||
content = final_video_url
|
||||
else:
|
||||
content = self._build_video_html(
|
||||
final_video_url, final_thumbnail_url
|
||||
)
|
||||
logger.info(f"Video generated: {video_url}")
|
||||
|
||||
except asyncio.CancelledError:
|
||||
|
||||
@@ -4,6 +4,10 @@ const byId = (id) => document.getElementById(id);
|
||||
const NUMERIC_FIELDS = new Set([
|
||||
'timeout',
|
||||
'max_retry',
|
||||
'retry_backoff_base',
|
||||
'retry_backoff_factor',
|
||||
'retry_backoff_max',
|
||||
'retry_budget',
|
||||
'refresh_interval_hours',
|
||||
'fail_threshold',
|
||||
'limit_mb',
|
||||
@@ -17,6 +21,8 @@ const NUMERIC_FIELDS = new Set([
|
||||
'usage_batch_size',
|
||||
'usage_max_tokens',
|
||||
'reload_interval_sec',
|
||||
'stream_idle_timeout',
|
||||
'video_idle_timeout',
|
||||
'nsfw_max_concurrent',
|
||||
'nsfw_batch_size',
|
||||
'nsfw_max_tokens'
|
||||
@@ -29,7 +35,7 @@ const LOCALE_MAP = {
|
||||
"app_key": { title: "后台密码", desc: "登录 Grok2API 服务管理后台的密码,请妥善保管。" },
|
||||
"app_url": { title: "应用地址", desc: "当前 Grok2API 服务的外部访问 URL,用于文件链接访问。" },
|
||||
"image_format": { title: "图片格式", desc: "生成的图片格式(url 或 base64)。" },
|
||||
"video_format": { title: "视频格式", desc: "生成的视频格式(仅支持 url)。" }
|
||||
"video_format": { title: "视频格式", desc: "生成的视频格式(html 或 url,url 为处理后的链接)。" }
|
||||
},
|
||||
"grok": {
|
||||
"label": "Grok 设置",
|
||||
@@ -43,7 +49,13 @@ const LOCALE_MAP = {
|
||||
"asset_proxy_url": { title: "资源代理 URL", desc: "代理请求到 Grok 官网的静态资源(图片/视频)地址。" },
|
||||
"cf_clearance": { title: "CF Clearance", desc: "Cloudflare 验证 Cookie,用于验证 Cloudflare 的验证。" },
|
||||
"max_retry": { title: "最大重试", desc: "请求 Grok 服务失败时的最大重试次数。" },
|
||||
"retry_status_codes": { title: "重试状态码", desc: "触发重试的 HTTP 状态码列表。" }
|
||||
"retry_status_codes": { title: "重试状态码", desc: "触发重试的 HTTP 状态码列表。" },
|
||||
"retry_backoff_base": { title: "退避基数", desc: "重试退避的基础延迟(秒)。" },
|
||||
"retry_backoff_factor": { title: "退避倍率", desc: "重试退避的指数放大系数。" },
|
||||
"retry_backoff_max": { title: "退避上限", desc: "单次重试等待的最大延迟(秒)。" },
|
||||
"retry_budget": { title: "退避预算", desc: "单次请求的最大重试总耗时(秒)。" },
|
||||
"stream_idle_timeout": { title: "流空闲超时", desc: "流式响应空闲超时(秒),超过将断开。" },
|
||||
"video_idle_timeout": { title: "视频空闲超时", desc: "视频生成空闲超时(秒),超过将断开。" }
|
||||
},
|
||||
"token": {
|
||||
"label": "Token 池设置",
|
||||
@@ -268,7 +280,8 @@ function renderConfig(data) {
|
||||
]);
|
||||
}
|
||||
else if (key === 'video_format') {
|
||||
built = buildSelectInput(section, key, 'url', [
|
||||
built = buildSelectInput(section, key, val, [
|
||||
{ val: 'html', text: 'HTML' },
|
||||
{ val: 'url', text: 'URL' }
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ app_url = "http://127.0.0.1:8000"
|
||||
app_key = "grok2api"
|
||||
api_key = ""
|
||||
image_format = "url"
|
||||
video_format = "url"
|
||||
video_format = "html"
|
||||
|
||||
[token]
|
||||
auto_refresh = true
|
||||
|
||||
@@ -10,6 +10,12 @@ asset_proxy_url = ""
|
||||
cf_clearance = ""
|
||||
max_retry = 3
|
||||
retry_status_codes = [401,429,403]
|
||||
retry_backoff_base = 0.5
|
||||
retry_backoff_factor = 2.0
|
||||
retry_backoff_max = 30.0
|
||||
retry_budget = 90.0
|
||||
stream_idle_timeout = 45.0
|
||||
video_idle_timeout = 90.0
|
||||
|
||||
[app]
|
||||
app_url = "http://127.0.0.1:8000"
|
||||
|
||||
+9
-3
@@ -99,8 +99,8 @@ curl http://localhost:8000/v1/chat/completions \
|
||||
| `thinking` | string | Thinking mode | `enabled`, `disabled`, `null` |
|
||||
| `video_config` | object | **Video model only** | - |
|
||||
| └─ `aspect_ratio` | string | Video aspect ratio | `16:9`, `9:16`, `1:1`, `2:3`, `3:2` |
|
||||
| └─ `video_length` | integer | Video length (seconds) | `5` - `15` |
|
||||
| └─ `resolution` | string | Resolution | `SD`, `HD` |
|
||||
| └─ `video_length` | integer | Video length (seconds) | `6`, `10` |
|
||||
| └─ `resolution_name` | string | Resolution | `480p`, `720p` |
|
||||
| └─ `preset` | string | Style preset | `fun`, `normal`, `spicy` |
|
||||
|
||||
Note: any other parameters will be discarded and ignored.
|
||||
@@ -157,7 +157,7 @@ Config file: `data/config.toml`
|
||||
| | `app_key` | Admin password | Password for the Grok2API admin panel. | `grok2api` |
|
||||
| | `api_key` | API key | Bearer token required to call Grok2API. | `""` |
|
||||
| | `image_format` | Image format | Output image format (`url` or `base64`). | `url` |
|
||||
| | `video_format` | Video format | Output video format (url only). | `url` |
|
||||
| | `video_format` | Video format | Output video format (html tag or processed url). | `html` |
|
||||
| **grok** | `temporary` | Temporary chat | Enable temporary conversation mode. | `true` |
|
||||
| | `stream` | Streaming | Enable streaming by default. | `true` |
|
||||
| | `thinking` | Thinking chain | Enable model thinking output. | `true` |
|
||||
@@ -169,6 +169,12 @@ Config file: `data/config.toml`
|
||||
| | `cf_clearance` | CF Clearance | Cloudflare clearance cookie for verification. | `""` |
|
||||
| | `max_retry` | Max retries | Max retries on Grok request failure. | `3` |
|
||||
| | `retry_status_codes` | Retry status codes | HTTP status codes that trigger retry. | `[401, 429, 403]` |
|
||||
| | `retry_backoff_base` | Backoff base | Base delay for retry backoff (seconds). | `0.5` |
|
||||
| | `retry_backoff_factor` | Backoff factor | Exponential multiplier for retry backoff. | `2.0` |
|
||||
| | `retry_backoff_max` | Backoff max | Max wait per retry (seconds). | `30.0` |
|
||||
| | `retry_budget` | Backoff budget | Max total retry time per request (seconds). | `90.0` |
|
||||
| | `stream_idle_timeout` | Stream idle timeout | Idle timeout for streaming responses (seconds). | `45.0` |
|
||||
| | `video_idle_timeout` | Video idle timeout | Idle timeout for video generation (seconds). | `90.0` |
|
||||
| **token** | `auto_refresh` | Auto refresh | Enable automatic token refresh. | `true` |
|
||||
| | `refresh_interval_hours` | Refresh interval | Token refresh interval (hours). | `8` |
|
||||
| | `fail_threshold` | Failure threshold | Consecutive failures before a token is disabled. | `5` |
|
||||
|
||||
@@ -112,8 +112,8 @@ curl http://localhost:8000/v1/chat/completions \
|
||||
| `thinking` | string | 思维链模式 | `enabled`, `disabled`, `null` |
|
||||
| `video_config` | object | **视频模型专用配置对象** | - |
|
||||
| └─`aspect_ratio` | string | 视频宽高比 | `16:9`, `9:16`, `1:1`, `2:3`, `3:2` |
|
||||
| └─`video_length` | integer | 视频时长 (秒) | `5` - `15` |
|
||||
| └─`resolution` | string | 分辨率 | `SD`, `HD` |
|
||||
| └─`video_length` | integer | 视频时长 (秒) | `6`, `10` |
|
||||
| └─`resolution_name` | string | 分辨率 | `480p`, `720p` |
|
||||
| └─`preset` | string | 风格预设 | `fun`, `normal`, `spicy`, `custom` |
|
||||
|
||||
**消息格式 (messages)**:
|
||||
@@ -190,7 +190,7 @@ curl http://localhost:8000/v1/images/generations \
|
||||
| | `app_key` | 后台密码 | 登录 Grok2API 服务管理后台的密码,请妥善保管。 | `grok2api` |
|
||||
| | `api_key` | API 密钥 | 调用 Grok2API 服务所需的 Bearer Token,请妥善保管。 | `grok2api` |
|
||||
| | `image_format` | 图片格式 | 生成的图片格式(url 或 base64)。 | `url` |
|
||||
| | `video_format` | 视频格式 | 生成的视频格式(仅支持 url)。 | `url` |
|
||||
| | `video_format` | 视频格式 | 生成的视频格式(html 或 url,url 为处理后的链接)。 | `html` |
|
||||
| **grok** | `temporary` | 临时对话 | 是否启用临时对话模式。 | `true` |
|
||||
| | `stream` | 流式响应 | 是否默认启用流式输出。 | `true` |
|
||||
| | `thinking` | 思维链 | 是否启用模型思维链输出。 | `true` |
|
||||
@@ -202,6 +202,12 @@ curl http://localhost:8000/v1/images/generations \
|
||||
| | `cf_clearance` | CF Clearance | Cloudflare 验证 Cookie,用于验证 Cloudflare 的验证。 | `""` |
|
||||
| | `max_retry` | 最大重试 | 请求 Grok 服务失败时的最大重试次数。 | `3` |
|
||||
| | `retry_status_codes` | 重试状态码 | 触发重试的 HTTP 状态码列表。 | `[401, 429, 403]` |
|
||||
| | `retry_backoff_base` | 退避基数 | 重试退避的基础延迟(秒)。 | `0.5` |
|
||||
| | `retry_backoff_factor` | 退避倍率 | 重试退避的指数放大系数。 | `2.0` |
|
||||
| | `retry_backoff_max` | 退避上限 | 单次重试等待的最大延迟(秒)。 | `30.0` |
|
||||
| | `retry_budget` | 退避预算 | 单次请求的最大重试总耗时(秒)。 | `90.0` |
|
||||
| | `stream_idle_timeout` | 流空闲超时 | 流式响应空闲超时(秒),超过将断开。 | `45.0` |
|
||||
| | `video_idle_timeout` | 视频空闲超时 | 视频生成空闲超时(秒),超过将断开。 | `90.0` |
|
||||
| **token** | `auto_refresh` | 自动刷新 | 是否开启 Token 自动刷新机制。 | `true` |
|
||||
| | `refresh_interval_hours` | 刷新间隔 | Token 刷新的时间间隔(小时)。 | `8` |
|
||||
| | `fail_threshold` | 失败阈值 | 单个 Token 连续失败多少次后被标记为不可用。 | `5` |
|
||||
|
||||
Reference in new issue
Block a user