feat(omni): 支持 GLM-4.6V 多模态模型(智谱 API) - #482
Conversation
- 新增 GlmAdapter: 继承 MiMoAdapter, 视频块/请求体同构 (video_url + fps +
media_resolution / thinking:disabled 实测兼容)
- get_adapter 按模型名子串匹配 ("glm" in model.lower()) 路由到 GlmAdapter,
glm-4.6v / glm-4.6v-flash / zhipu/glm-4.6v 均命中
- GLM-4.6V 是纯视觉模型, 不支持 input_audio (音频降级见后续 commit);
X-Title 由用户经 model.omni.extra_headers 配置 (见后续 commit)
|
👋 感谢提交 PR @0xhyperdan!维护者会尽快 review。 提交前请确认:
|
X-Title: 4.5V MCP Local 是 Coding Plan Key 必须携带的标识, 不带会按普通资源包计费并返回 429 余额不足。
[PR #482]: feat(omni): 支持 GLM-4.6V 多模态模型(智谱 API)作者: 0xhyperdan 修改方案要解决的问题 智谱 GLM-4.6V 走的是 OpenAI 兼容协议,看起来可以直接复用现有的 MiMo 适配器,但有两处不兼容会让接入直接失败:一是 GLM-4.6V 是纯视觉语言模型,收到 整体方案 三条主线互相独立:一条改推理时的路由分叉,一条打通自定义请求头的配置通道,一条把这份配置接进 Web 管理端。 主线 1 — 给"这家模型能不能听声音"加一个能力位,纯音频窗口在听不见的模型上退回按视频处理
主线 2 — 加一份用户自定义请求头,从配置文件一路透传到每一个发请求的地方
主线 3 — Web 管理端的档案读写与探测都带上这份头
关键设计原则
测试覆盖
问题
🔴 严重(提交前必须修复)
🟡 重要(应当修复)
🔵 建议(可选优化)
结论需要修改 — 三个 🔴 里,图视混发那条会让默认配置下的 GLM 接入在第一个有人活动的窗口就 400,直接阻断本 PR 声称的能力;路由翻转半截导致非 fused 路径下规则告警在 GLM 上静默全灭;Web 保存清空自定义头则让主线 2 的整条配置通道在实际使用中不成立(且目前 Web / CLI / 文档三条路都配不了这个字段,建议本 PR 内至少打通一条)。四个 🟡 中「测试连接 / 拉取模型列表把头发往任意 base_url」是本 PR 新引入的外发面,与仓库自己在 由 review-pr skill v1.6 生成 |
pr-review 发现的问题修复:
🔴 GLM-4.6V 是纯视觉模型(文本/图片/视频/文件), input_audio 属于
GLM-4-Voice/GLM-Realtime 线, 发给 4.6V 稳定 400 → 夜间"有声无画面
变化"窗口会把熔断打进需人工干预的 CONFIG 态:
- OmniProviderAdapter 新增 supports_audio_input 能力位(默认 True)
- GlmAdapter 置 False
- prompt_builder audio route / omni_client on_demand 路径按能力位
降级为 text-only, 不发音频块
🟡 probe.fetch_models / probe_omni 预检硬编码 Bearer, 漏掉 GLM 的
X-Title 头(429 → 模型下拉列表拉不出来): 按 hostname 判断补头,
与 adapter.auth_headers 保持一致
🔵 注释/文档: 3 处 OpenAI 兼容族枚举补 GLM; singleton 测试改用
glm-4.6v-flash 覆盖同族复用; PR 描述改子串匹配口径
实测: GLM-4.6V 拒 m4a(400 format 不支持), 尝试 wav 也报 base64 格式
错误, 音频输入不可用 → 降级是正确方案
pr-review 第二轮意见修复:
🔴 GLM Coding Plan 的 X-Title 头是计费口径/服务条款层面的选择,
不应由 adapter 硬编码且无法关闭:
- OmniModelSettings / OmniConfig 新增 extra_headers 字段(默认空)
- GlmAdapter 移除 auth_headers 覆写, 退回纯继承(仅保留
supports_audio_input = False)
- omni.py / omni_client.py 三处请求头组装合并 config.extra_headers
- probe.py 移除硬编码 X-Title, 还原纯净实现
🟡 supports_audio_input 只挡了音频块, 没挡 prompt 里"本轮有音频"
的声明 — GLM 每个 audio-only 窗口会收到"请转录音频"的 system
prompt 却没有音频(空烧钱 + 可能脑补 speeches):
- 新增 _adapter_hears_audio() 助手
- audio 路由对听不见音频的 provider 退回 video 路由(帧还在)
- video 路由 has_audio/has_speech 按听力置 False, 既有机制剥掉
speeches/env_sounds
- build_prompt 系列 / run_omni 系列透传 adapter
🔵 基类 docstring 修正; 新增 extra_headers 生效/默认测试
测试: omni + probe + config 398 个全过
_propagate_model_omni_to_perception 下推时漏了 extra_headers 字段, OmniConfig 拿不到用户配置的附加请求头(X-Title 等)
pr-review 第三轮意见修复:
🔴 探测链路(probe.py)没接 extra_headers — Coding Plan key 的探测
请求 100% 429, 导致: 配置存不进去 / 模型下拉为空 / 熔断后自愈
永久失效 / 手动重试也救不回来:
- fetch_models / probe_chat / probe_omni 各加 extra_headers 参数
- router.py 5 处调用点传入(OmniConfigBody 新增 extra_headers 字段,
按 label 查档案 extra_headers, 缺省用当前 active)
🟡 resolve_live_omni_config 热更新漏 extra_headers — 改配置后
请求头不生效, 需重启进程:
- replace() 补 extra_headers=dict(o.extra_headers)
🟡 音频路降级零测试覆盖 — 新增 TestGlmAudioFallback 4 用例:
audio-only 退回 video 路由 / MiMo 保持音频路 / fused 路径降级 /
video 路由 GLM 剥掉 speeches 字段
🔵 注释口径修正(降级 text-only → 退回 video 路由) + get_adapter
docstring 修正(鉴权头相同, 唯一差异是 supports_audio_input)
🔵 mp4 音轨编码与 has_audio 同判据 — _packet_audio_included /
_encode_video / _encode_batch_video 传 adapter, GLM 视频路不混
音轨, 避免白传模型解码不了的 AAC
测试: omni + config 相关 395 个全过
d7ca61e to
b374bdd
Compare
pr-review 第四轮意见修复(同一字段的遗漏点): 🔴 probe_omni 内部 chat 回退路径漏传 extra_headers — GET /models 通过后最后一跳 probe_chat 不带 X-Title → 429, 保存/激活/测试/ 重试全失败 🔴 processor._run_omni_probe 熔断自动探活漏传 — GLM 感知一次 抖动后熔断半开探活永远 429, 永久停摆 🔴 activate_omni_config 写回 active 漏 extra_headers — 激活 GLM 档案后推理立即 429 🟡 _profiles_as_dicts 不返回 extra_headers — DELETE 清零所有 剩余档案的自定义头 🟡 _full_omni_payload 不返回 — 前端编辑无法回显, 保存即丢 测试: omni + admin 434 个全过
processor._run_omni_probe 现在传 extra_headers, 测试 mock 需同步
pr-review 第五轮意见: 🟡 test_omni_config 的 extra_headers 取法与 list_omni_models 不一致 — 测试非 active 的 GLM 档案时 429, 统一按 label 查档案/缺省 active 🔵 probe.py extra_headers 路径零测试 — 新增 3 用例锁住 5 处合并点 (fetch_models / probe_chat / probe_omni 两跳), 防重构静默回归
0069045 to
7d99692
Compare
|
hi please resolve conflicts :) |
|
感谢 PR! 「新增 provider」和「provider 能力差异」两件事分开处理、能力位默认放行让三家老 provider 行为零变化、 验证情况:在 PR head 另外我用真实智谱 key 对 有一条阻塞问题,另有两处疑似 bug、一条文档建议、一个想和你讨论的取舍,最后是几条小事。 🔴 GLM 不接受图 + 视频同请求,而 fused 路径就是这么发的实测( 复现——只发图或只发视频都正常 200,混发才拒,调整块顺序也无效: curl -s https://open.bigmodel.cn/api/paas/v4/chat/completions \
-H "Authorization: Bearer $ZHIPU_KEY" -H "Content-Type: application/json" \
-d '{"model":"glm-4.6v","messages":[{"role":"user","content":[
{"type":"image_url","image_url":{"url":"data:image/png;base64,<小图>"}},
{"type":"video_url","video_url":{"url":"data:video/mp4;base64,<小视频>"}}]}]}'
所以等这个 PR rebase 到当前 main 之后,只要走 fused,每个窗口都会被拒,跟库里有没有人无关了。 只有 fused 会混,其他路径不会:两条非 fused 主路径的 这个混发结构不是本 PR 引入的——MiMo / Qwen 走同一段代码,它们接受混发所以一直没暴露。只是把 GLM 接上这条链路正好是本 PR 做的事,所以想借这个 PR 一起把它收掉。 建议的方向:加一个模型能力位,把相关功能按能力关掉从上面能看出来,注入图片块的地方现在有三处,分属两个独立开关的功能(identity/fused、Smart Crop)外加宠物参考图。所以这不是"改一处组装"能收掉的问题,而是**「模型能力 × 功能开关」的兼容性约束**——而且 Smart Crop 对 GLM 也没法「降级保留」:它的价值就是「全景图给上下文 + 裁切视频给细节」这个配对,把全景图去掉,裁切就失去了安全网。所以对不支持混发的模型,这些功能只能直接关掉、并且不允许开启,降级保留没有意义。 好消息是这个模式仓库里已经有了: 具体建议:
这两条是我觉得本 PR 内比较值得一起处理的最小集,做完 GLM 就不会再 400 了。下面几条属于 UX 层、跨 backend/admin/web 三层,可以放到后续单独 PR,我们也可以接手,你不用在这个 PR 里扛:
顺带一个信息: 🔴 Web 保存 omni 配置会清空
|
# Conflicts: # backend/miloco/src/miloco/perception/engine/omni/prompt_builder.py # backend/miloco/tests/perception/engine/omni/test_prompt_builder.py
There was a problem hiding this comment.
Pull request overview
本 PR 为 Miloco 感知引擎的 omni 模型新增智谱 GLM-4.6V(OpenAI 兼容协议)接入,并围绕 GLM “不支持音频输入” 的限制引入路由级音频降级,同时增加 extra_headers 配置能力以支持 GLM Coding Plan 的 X-Title 头透传,确保探测链路与推理链路一致。
Changes:
- 新增
GlmAdapter(继承MiMoAdapter)并在get_adapter()中基于模型名子串路由到 GLM。 - 增加 provider 能力位
supports_audio_input,对不支持音频的 provider(GLM)在 audio-only 窗口回退到 video 路由,并在消息组装层避免发送input_audio。 - 在
OmniModelSettings/OmniConfig增加extra_headers,并在探测与推理请求头组装处统一合并透传(默认不携带)。
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| backend/miloco/src/miloco/perception/engine/omni/provider.py | 新增 GlmAdapter 与 GLM 路由,并通过能力位标记不支持音频输入 |
| backend/miloco/src/miloco/perception/engine/omni/prompt_builder.py | 基于 adapter 能力实现 audio-only → video 回退,并同步剥离音频相关 schema/提示与 mp4 音轨合成 |
| backend/miloco/src/miloco/perception/engine/omni/omni_client.py | 推理请求头合并 config.extra_headers;消息构建层按 supports_audio_input 兜底不发音频块;live config 透传 extra_headers |
| backend/miloco/src/miloco/perception/engine/omni/omni.py | run_omni* 构建 prompt 时传入 adapter;请求头合并 extra_headers |
| backend/miloco/src/miloco/perception/engine/omni/probe.py | probe/fetch_models/probe_chat/probe_omni 全链路透传 extra_headers |
| backend/miloco/src/miloco/perception/processor.py | probe 调用透传 extra_headers 以匹配实际推理配置 |
| backend/miloco/src/miloco/perception/engine/config.py | OmniConfig 增加 extra_headers 配置字段 |
| backend/miloco/src/miloco/config/settings.py | OmniModelSettings 增加 extra_headers 并下推到 perception.engine.omni |
| backend/miloco/src/miloco/admin/router.py | 管理端配置/激活/探测/拉模型列表接口透传并回显 extra_headers |
| backend/miloco/tests/perception/engine/omni/test_provider.py | 覆盖 GLM 路由、OpenAICompat 归属、单例、鉴权头与音频能力位 |
| backend/miloco/tests/perception/engine/omni/test_prompt_builder.py | 覆盖 GLM 音频降级行为(audio-only 回退 video、fused 同步降级、prompt 剥离音频字段) |
| backend/miloco/tests/perception/engine/omni/test_probe.py | 覆盖 probe/fetch_models/probe_chat/probe_omni 的 extra_headers 透传 |
| backend/miloco/tests/perception/engine/omni/test_omni_client_circuit.py | 覆盖 call_omni 头合并与 resolve_live_config 的 extra_headers 行为 |
| backend/miloco/tests/perception/test_omni_probe_tick_drive.py | 更新 tick-drive probe 桩函数签名与 fake settings 以支持 extra_headers |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| class _FakeOmni: | ||
| model = "m" | ||
| base_url = "https://x/v1" | ||
| api_key = "sk-x" | ||
| extra_headers: dict[str, str] = {} |
| class _Mo: | ||
| model = "m1" | ||
| base_url = "https://x/v1" | ||
| api_key = "sk-NEW" | ||
| extra_headers: dict[str, str] = {} |
背景
Miloco 感知引擎目前仅支持 MiMo(默认)/ Qwen / Gemini 三类 omni 模型。用户希望接入智谱 GLM 多模态模型(如 GLM-4.6V)。
改动
在
provider.py新增GlmAdapter(继承MiMoAdapter):video_url + fps + media_resolution、thinking: disabled等字段(已实测兼容,含流式)input_audio属于 GLM-4-Voice / GLM-Realtime 线——实测 m4a / wav 均被 400 拒get_adapter()按模型名子串匹配("glm" in model.lower())路由到GlmAdapter,故glm-4.6v/glm-4.6v-flash/zhipu/glm-4.6v均命中音频降级(audio-only 窗口)
GLM 不支持
input_audio,若夜间「有声音、画面静止」的窗口照发音频块会稳定 400,把熔断打进需人工干预的 CONFIG 态。修复:OmniProviderAdapter新增supports_audio_input能力位(默认 True,现有 adapter 行为不变);GlmAdapter置Falsehas_audio/has_speech标注同步置 False——prompt 不再声称「本轮有音频」,既有机制会把 speeches / env_sounds 从 schema 剥掉,避免空烧钱和脑补omni_clienton_demand 路径按能力位不发音频块build_prompt系列 /run_omni系列透传 adapterGLM Coding Plan 的 X-Title 头:用户配置,默认不携带
X-Title: 4.5V MCP Local是智谱 GLM Coding Plan 的 MCP 流量标识,Coding Plan Key 必须携带否则 429(余额不足)。这是计费口径 / 服务条款层面的选择,不作为仓库默认行为:OmniModelSettings/OmniConfig新增extra_headers字段(默认空)config.extra_headers;model.omni → perception.engine下推 validator 同步透传该字段{ "model": { "omni": { "api_key": "<Key>", "model": "glm-4.6v", "base_url": "https://open.bigmodel.cn/api/paas/v4", "extra_headers": { "X-Title": "4.5V MCP Local" } } } }/api/paas/v4(Coding Plan 的/api/coding/paas/v4不支持视觉输入)测试
TestGlmAdapter:路由(含大小写不敏感)、OpenAI 兼容族归属、supports_audio_input = False、auth_headers 纯 Bearer(不硬编码 X-Title)extra_headers生效/默认两个客户端测试input_audio块glm-4.6vvsglm-4.6v-flash