diff --git a/app/services/grok/services/image.py b/app/services/grok/services/image.py index 5bd25aacb..0f46c2bb2 100644 --- a/app/services/grok/services/image.py +++ b/app/services/grok/services/image.py @@ -138,6 +138,7 @@ async def _stream_retry() -> AsyncGenerator[str, None]: token_mgr=token_mgr, token=current_token, model_info=model_info, + tried_tokens=tried_tokens, prompt=prompt, n=n, response_format=response_format, @@ -212,6 +213,7 @@ async def _collect_ws( token_mgr: Any, token: str, model_info: Any, + tried_tokens: set[str], prompt: str, n: int, response_format: str, @@ -226,9 +228,9 @@ async def _collect_ws( calls_needed = max(1, int(math.ceil(n / expected_per_call))) calls_needed = min(calls_needed, n) - async def _fetch_batch(call_target: int): + async def _fetch_batch(call_target: int, call_token: str): upstream = image_service.stream( - token=token, + token=call_token, prompt=prompt, aspect_ratio=aspect_ratio, n=call_target, @@ -246,7 +248,7 @@ async def _fetch_batch(call_target: int): for i in range(calls_needed): remaining = n - (i * expected_per_call) call_target = min(expected_per_call, remaining) - tasks.append(_fetch_batch(call_target)) + tasks.append(_fetch_batch(call_target, token)) results = await asyncio.gather(*tasks, return_exceptions=True) for batch in results: @@ -268,16 +270,47 @@ async def _fetch_batch(call_target: int): remaining = n - len(all_images) extra_attempts = int(get_config("image.blocked_parallel_attempts") or 5) extra_attempts = max(0, min(extra_attempts, 10)) + parallel_enabled = bool(get_config("image.blocked_parallel_enabled", True)) if extra_attempts > 0: logger.warning( f"Image finals insufficient ({len(all_images)}/{n}), running " - f"{extra_attempts} parallel recovery attempts for remaining={remaining}" + f"{extra_attempts} recovery attempts for remaining={remaining}, " + f"parallel_enabled={parallel_enabled}" ) - extra_tasks = [ - _fetch_batch(min(expected_per_call, remaining)) - for _ in range(extra_attempts) - ] - extra_results = await asyncio.gather(*extra_tasks, return_exceptions=True) + extra_tasks = [] + if parallel_enabled: + recovery_tried = set(tried_tokens) + recovery_tokens: List[str] = [] + for _ in range(extra_attempts): + recovery_token = await pick_token( + token_mgr, + model_info.model_id, + recovery_tried, + ) + if not recovery_token: + break + recovery_tried.add(recovery_token) + recovery_tokens.append(recovery_token) + + if recovery_tokens: + logger.info( + f"Recovery using {len(recovery_tokens)} distinct tokens" + ) + for recovery_token in recovery_tokens: + extra_tasks.append( + _fetch_batch(min(expected_per_call, remaining), recovery_token) + ) + else: + extra_tasks = [ + _fetch_batch(min(expected_per_call, remaining), token) + for _ in range(extra_attempts) + ] + + if not extra_tasks: + logger.warning("No tokens available for recovery attempts") + extra_results = [] + else: + extra_results = await asyncio.gather(*extra_tasks, return_exceptions=True) for batch in extra_results: if isinstance(batch, Exception): logger.warning(f"WS recovery batch failed: {batch}") diff --git a/app/static/admin/js/config.js b/app/static/admin/js/config.js index 48cd1a8f5..0eaf3f419 100644 --- a/app/static/admin/js/config.js +++ b/app/static/admin/js/config.js @@ -98,6 +98,7 @@ const LOCALE_MAP = { "nsfw": { title: "NSFW 模式", desc: "WebSocket 请求是否启用 NSFW。" }, "medium_min_bytes": { title: "中等图最小字节", desc: "判定中等质量图的最小字节数。" }, "final_min_bytes": { title: "最终图最小字节", desc: "判定最终图的最小字节数(通常 JPG > 100KB)。" }, + "blocked_parallel_enabled": { title: "启用并行补偿", desc: "疑似审查/拦截时,是否启用并行补偿生成。" }, "blocked_parallel_attempts": { title: "拦截补偿并发次数", desc: "疑似审查/拦截导致无最终图时,自动并行补偿生成次数。" } }, diff --git a/config.defaults.toml b/config.defaults.toml index 61a7aa36d..1784c9748 100644 --- a/config.defaults.toml +++ b/config.defaults.toml @@ -111,6 +111,8 @@ medium_min_bytes = 30000 final_min_bytes = 100000 # 遇到疑似审查/拦截时的并行补偿生成次数 blocked_parallel_attempts = 5 +# 是否启用并行补偿(启用时优先使用不同 token) +blocked_parallel_enabled = true # ==================== SuperImage 配置 ==================== diff --git a/readme.md b/readme.md index 4e13f9557..4d9e9fdcd 100644 --- a/readme.md +++ b/readme.md @@ -181,7 +181,7 @@ curl http://localhost:8000/v1/chat/completions \ - `grok-superimage-1.0` 与瀑布流 imagine 生成链路一致,可直接通过 `/v1/chat/completions` 调用;其 `n/size/response_format` 由服务端 `[superimage]` 统一控制。 - `grok-superimage-1.0` 在 `/v1/chat/completions` 的流式输出仅返回最终成图,不返回中间预览图。 - `grok-superimage-1.0` 流式 URL 出图会保持原始图片名(不追加 `-final` 后缀)。 -- 当图片疑似被审查拦截导致无最终图时,服务端会按 `image.blocked_parallel_attempts` 自动并行补偿生成;若仍无满足 `image.final_min_bytes` 的最终图则返回失败。 +- 当图片疑似被审查拦截导致无最终图时,若开启 `image.blocked_parallel_enabled`,服务端会按 `image.blocked_parallel_attempts` 自动并行补偿生成,并优先使用不同 token;若仍无满足 `image.final_min_bytes` 的最终图则返回失败。 - `grok-imagine-1.0-edit` 必须提供图片,多图默认取**最后 3 张**与最后一个文本。 - `grok-imagine-1.0-video` 支持文生视频与图生视频(通过 `image_url` 传参考图,**仅取第 1 张**)。 - 除上述外的其他参数将自动丢弃并忽略。