Skip to content

[Bug]: Provider 层缺陷汇总:health_check 假阳性 / Volcengine 视频 30s 硬超时且 ref 被丢弃 / DeepSeek 忽略 timeout #250

Description

@LyaQanYi

概述

对 v2.30.0(1228ac2)做代码审查时,在 core/provider/ 发现若干缺陷。最需要关注的是第 1 条 —— 它会让 WebUI 的"测试模型"按钮在配置完全错误时依然显示成功。


1. TTS/STT/Embedding/Rerank 系统性吞异常返回空值,导致 health_check 假阳性(High)

位置(均为 except Exception 后仅打日志并返回空值):

  • Embedding 返回 []:core/provider/src/openai/model_clients.py:337-340、core/provider/src/aliyun_bailian/model_clients.py:258-263、core/provider/src/volcengine/model_clients.py:189-194、core/provider/src/siliconflow_cn/model_clients.py:109-114、core/provider/src/modelscope/model_clients.py:120-125
  • Rerank 返回 []:core/provider/src/aliyun_bailian/model_clients.py:348-350(api_key 缺失时 :286-288 同样返回 [])
  • TTS 返回 None:core/provider/src/aliyun_bailian/model_clients.py:2101-2109、core/provider/src/gptsovits/model_clients.py:87-100
  • STT 返回 "":core/provider/src/aliyun_bailian/model_clients.py:1673-1678

典型形态:

except Exception as e:
    logger.error(f"Embedding error: {e}")
    return []

实际行为分两层:

  1. ProviderManager.health_check(core/provider/provider_manager.py:518)以"不抛异常即成功"判定健康状态,而 TTS 返回 None、embed/rerank 返回 [] 都不抛异常。也就是说 API key 填错、网络不通、模型 ID 不存在,WebUI 的"测试模型"照样显示 success 和延迟数值(经 webui/routes/providers.py:448 暴露给前端)。用户会误以为配置正确,直到实际使用才发现无声失败。
  2. Embedding 客户端通过 plugin_context.get_default_embedding_client() 暴露给插件/记忆系统,调用方拿到 [] 无法区分"provider 挂了"和"没有结果";返回长度与输入 texts 数量也不一致,若调用方按索引 zip 对应,会在 RAG/记忆管道里造成静默数据损坏。

复现:随便填一个错误的 API key,在 WebUI 里测试该 provider 的 TTS 或 Embedding 模型 —— 显示成功。

期望行为:统一抛 ProviderAPIError。仓库里该异常类与 agent_executor 的故障切换机制都已就绪(core/agent/agent_executor.py:106 同时捕获 openai SDK 异常和 ProviderAPIError),anthropic 客户端(anthropic/model_clients.py:296-297、:410-411)已是正确写法,只是这些客户端没有沿用。


2. Volcengine 视频生成轮询上限硬编码 30 秒,功能基本不可用(High)

位置:core/provider/src/volcengine/model_clients.py:71、:78

async with httpx.AsyncClient(timeout=30) as client:
    ...
    while time.time() - start_ts < 30:

实际行为:火山方舟(Seedance 等)视频生成任务通常需要 1–5 分钟,30 秒内几乎不可能到达 succeeded,因此 generate_video 几乎必然走到 :100 的 raise TimeoutError。任务在服务端可能最终成功,但客户端总是提前放弃。

而 schema.json 中 video 段为空,用户没有任何办法调大这个时限。对比同文件的 embedding 客户端(:155-163 有完整的 timeout 读取与校验),这里显然是遗漏。

期望行为:把轮询上限做成可配置项(参考 embedding 的实现),默认值给到分钟级。


3. Volcengine generate_video 静默丢弃参考图 ref 参数(High)

位置:core/provider/src/volcengine/model_clients.py:69-72、:102-119

async def generate_video(self, prompt: str, ref: list[Image] = None, duration: int = 5, **kwargs) -> Video:
    async with httpx.AsyncClient(timeout=30) as client:
        task_id = await self._create_task(client=client, text=prompt, ref=ref, duration=duration)

实际行为:_create_task 签名接收了 ref,但它构造的 json_data(:108-119)只有一个 {"type": "text", "text": text} 内容项,ref 从未被写入请求体(另外 ratio 被硬编码为 "16:9")。传入参考图希望做图生视频时,请求被静默降级为纯文生视频,没有任何日志或报错,用户拿到的视频与参考图毫无关系且无从排查。

期望行为:把 ref 序列化进 content 数组;ratio 也建议开放配置。


4. DeepSeek 客户端完全忽略用户配置的 timeout(High)

位置:core/provider/src/deepseek/model_clients.py:33-39(_build_client 无超时)、:62-67(请求 kwargs 缺 timeout)

kwargs = dict(
    model=self.model.model_id,
    messages=[m if isinstance(m, dict) else m.to_dict() for m in request.messages],
    tools=request.tools if request.tools else NOT_GIVEN,
    tool_choice=request.tool_choice if request.tool_choice != "none" else NOT_GIVEN,
)

实际行为:schema.json:44-53 明确定义了 model_config.llm.timeout(默认 120 秒)并在 WebUI 展示给用户,但 _build_request_kwargs 从不读取它,_build_client() 也没设客户端级超时。于是实际使用 openai SDK 默认的 600 秒 —— 一次网络挂起会把会话卡住最长 10 分钟才触发模型故障切换,用户配置的 120 秒形同虚设且无任何提示。

对比它模仿的 OpenAICompatibleLLMClient._build_request_kwargs(core/utils/model_clients.py:58、:68)正确透传了 timeout。

期望行为:与 OpenAI 兼容实现保持一致,透传 timeout。


5. 大量 AsyncOpenAI 客户端按请求创建且从不关闭(Medium)

位置(每次调用新建 AsyncOpenAI,内部持有 httpx.AsyncClient 与连接池,既不 close() 也不用 async with):

  • core/provider/src/deepseek/model_clients.py:82、:133(主 LLM 路径)
  • core/provider/src/openai/model_clients.py:54-59、:319-324
  • core/provider/src/volcengine/model_clients.py:25-28、:47-50、:174-178
  • core/provider/src/siliconflow_cn/model_clients.py:94-98
  • core/provider/src/modelscope/model_clients.py:105-109
  • core/utils/model_clients.py:76、:127(所有 OpenAI 兼容 provider 共用)、:204-208

实际行为:清理只能依赖 GC 兜底。高并发下文件描述符堆积,且每个请求都要重做 TCP+TLS 握手(无 keep-alive 复用),延迟与资源开销被放大。

期望行为:改用 async with。同仓库已有示范:aliyun_bailian/model_clients.py:233-237、anthropic 客户端 :294、:320。


6. get_model_client 在 provider 实例化失败时抛出误导性 AttributeError(Medium)

位置:core/provider/provider_manager.py:94-100

provider = self.get_provider(provider_id)
model_info = self.get_model_info(provider_id, model_id, model_type)
if not model_info:
    return
model_type_enum = model_info.model_type

if model_type_enum not in provider.models:

实际行为:get_provider 返回 Optional[BaseProvider](:799-801),但这里没有判空。若 provider 配置存在于 kira_config(所以 get_model_info 能正常返回)但启动时 set_provider 实例化失败(:793-794 仅记录日志、不写入 _providers),此后任何取该 provider 模型的调用都会抛 AttributeError: 'NoneType' object has no attribute 'models',在 health_check 里表现为一条毫无信息量的错误串,掩盖了"provider 加载失败"这一真实根因。

期望行为:判空并抛出明确的"provider 未加载"错误。


7. fetch_remote_models 的 model_type 参数被忽略(Low)

位置:core/provider/provider_manager.py:807、:816

async def fetch_remote_models(self, provider_id: str, model_type: str = "llm") -> list[dict]:
    ...
    models = await provider.get_llm_list()

实际行为:签名接受 model_type,但无论传什么都只调 get_llm_list()。WebUI 以 model_type 调用(webui/routes/providers.py:459),前端请求 tts/image 等非 LLM 类型的远程模型列表时,返回的其实是 LLM 列表,用户会在错误类别下看到不匹配的模型。


补充:其他 Low 级观察

  • provider_manager.py:214 的 if default_model and ":" in default_model: 对不含冒号的配置值静默返回 None,随后 get_default_llm 在 :110 抛 AttributeError,而不是给出清晰的 ValueError。
  • core/provider/provider.py:203-206 的 get_model_client 返回类对象,但类型标注为 BaseModelClient 实例,标注与实现不符。
  • siliconflow_cn/model_clients.py:46 的 response.json().get("images")[0].get("url") 无防护,字段缺失时 TypeError/IndexError 会掩盖服务端真实错误信息;:159-166 rerank 的 documents[item["index"]] 无越界保护(aliyun_bailian 的 :364-372 是防御式写法,可对照)。volcengine/model_clients.py:40、:62 的 images_response.data[0].url 同样未判空(openai 客户端 :80-81 有检查)。
  • siliconflow_cn/model_clients.py:65 把所有音频硬编码为 audio.wav,而 IM 场景语音多为 amr/silk/ogg。
  • aliyun_bailian/model_clients.py:38、:2155 的全局 threading.Lock 把 CosyVoice TTS 串行化为并发 1,多会话同时触发时排队明显,建议换成有界信号量。
  • OpenAI 兼容/DeepSeek 的 chat_stream 会产出两个 is_final=True 的 chunk(usage-only 与 finish_reason 各一个),与 anthropic 的单一最终 chunk 语义不一致;另外无条件发送 stream_options={"include_usage": True},部分自称兼容的网关会直接拒绝该参数。
  • 测试覆盖:aliyun_bailian/model_clients.py(2257 行)零测试,其中 _clamp_wh/_build_size_pixels/_resolve_image_size、_normalize_dialect、_detect_language_hint、rerank 双协议归一化、_extract_image_url 全是无 IO 依赖的纯函数;deepseek/volcengine/siliconflow_cn/modelscope/gptsovits 五个 provider 也零测试 —— 上面第 2/3/4 条本可被最基础的"请求体构造正确性"测试捕获。

KiraAI version: v2.30.0(1228ac2)
Environment: 静态代码审查,非特定 OS/Python 版本相关

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions