概述
对 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 []
实际行为分两层:
ProviderManager.health_check(core/provider/provider_manager.py:518)以"不抛异常即成功"判定健康状态,而 TTS 返回 None、embed/rerank 返回 [] 都不抛异常。也就是说 API key 填错、网络不通、模型 ID 不存在,WebUI 的"测试模型"照样显示 success 和延迟数值(经 webui/routes/providers.py:448 暴露给前端)。用户会误以为配置正确,直到实际使用才发现无声失败。
- 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 版本相关
概述
对
v2.30.0(1228ac2)做代码审查时,在core/provider/发现若干缺陷。最需要关注的是第 1 条 —— 它会让 WebUI 的"测试模型"按钮在配置完全错误时依然显示成功。1. TTS/STT/Embedding/Rerank 系统性吞异常返回空值,导致
health_check假阳性(High)位置(均为
except Exception后仅打日志并返回空值):[]: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[]:core/provider/src/aliyun_bailian/model_clients.py:348-350(api_key 缺失时:286-288同样返回[])None:core/provider/src/aliyun_bailian/model_clients.py:2101-2109、core/provider/src/gptsovits/model_clients.py:87-100"":core/provider/src/aliyun_bailian/model_clients.py:1673-1678典型形态:
实际行为分两层:
ProviderManager.health_check(core/provider/provider_manager.py:518)以"不抛异常即成功"判定健康状态,而 TTS 返回None、embed/rerank返回[]都不抛异常。也就是说 API key 填错、网络不通、模型 ID 不存在,WebUI 的"测试模型"照样显示 success 和延迟数值(经webui/routes/providers.py:448暴露给前端)。用户会误以为配置正确,直到实际使用才发现无声失败。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实际行为:火山方舟(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实际行为:
_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)实际行为:
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-324core/provider/src/volcengine/model_clients.py:25-28、:47-50、:174-178core/provider/src/siliconflow_cn/model_clients.py:94-98core/provider/src/modelscope/model_clients.py:105-109core/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实际行为:
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实际行为:签名接受
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-166rerank 的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,多会话同时触发时排队明显,建议换成有界信号量。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 版本相关