openclaw配置修改支持图片识别
- 内容介绍
- 文章标签
- 相关推荐
- 备份配置:读取目标配置(默认
~/.openclaw/openclaw.json,也可传入路径),并立刻写出备份openclaw.json.bak(覆盖旧备份)。 - 补齐模型图像能力声明:遍历
models.providers.*.models[],确保每个模型条目都有input字段,且包含text与image;缺什么补什么,并去重、规范化。 - 修正默认图片模型指向:检查
agents.defaults.imageModel.primary是否指向一个真实存在且input含image的provider/model;不合法就自动挑一个可用的(优先agents.defaults.model.primary,再尝试 imageModel 的 fallback,最后选第一个支持 image 的 provider 模型)。 - 处理 allowlist(如果你启用了):如果存在
agents.defaults.models(模型 allowlist),会确保上一步选中的imageModel.primary在 allowlist 里,避免“模型存在但被禁止使用”。 - iMessage 附件开关(如果你配置了 iMessage):如果存在
channels.imessage,确保includeAttachments为true(缺失就加,false 就改 true)。
执行结果:
- 会把修正后的 JSON 写回原配置文件,并打印扫描了多少 provider/model、修改了多少模型 input、imageModel 是否被修正,以及 iMessage includeAttachments 的处理结果。
测试结果: 在虚拟机中搭建测试用openclaw 直接调用脚本修改成功在openclaw网页识图
image1206×2622 446 KB
image1620×413 38.4 KB
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import json
import os
import sys
from typing import Any, Dict, List, Optional, Tuple
DEFAULT_PATH = os.path.expanduser("~/.openclaw/openclaw.json")
def ensure_list_of_strings(v: Any) -> List[str]:
if isinstance(v, list):
out: List[str] = []
for x in v:
if isinstance(x, str):
s = x.strip()
if s:
out.append(s)
return out
if isinstance(v, str):
s = v.strip()
return [s] if s else []
return []
def dedupe_case_insensitive(items: List[str]) -> List[str]:
seen = set()
out: List[str] = []
for x in items:
k = x.lower()
if k in seen:
continue
seen.add(k)
out.append(x)
return out
def norm_ref(ref: str) -> str:
return str(ref or "").strip().lower()
def build_provider_model_index(cfg: Dict[str, Any]) -> Tuple[Dict[str, str], Dict[str, List[str]]]:
"""
Returns:
- canonical_by_norm: map of "provider/model" (lowercased) -> canonical "Provider/model"
- input_by_norm: map of norm ref -> list of input strings (lowercased)
"""
canonical_by_norm: Dict[str, str] = {}
input_by_norm: Dict[str, List[str]] = {}
models = cfg.get("models")
if not isinstance(models, dict):
return canonical_by_norm, input_by_norm
providers = models.get("providers")
if not isinstance(providers, dict):
return canonical_by_norm, input_by_norm
for provider_key, provider_cfg in providers.items():
if not isinstance(provider_cfg, dict):
continue
model_list = provider_cfg.get("models")
if not isinstance(model_list, list):
continue
for m in model_list:
if not isinstance(m, dict):
continue
mid = m.get("id")
if not isinstance(mid, str) or not mid.strip():
continue
canonical = f"{provider_key}/{mid.strip()}"
n = norm_ref(canonical)
if n not in canonical_by_norm:
canonical_by_norm[n] = canonical
inp = ensure_list_of_strings(m.get("input"))
input_by_norm[n] = [x.lower() for x in inp]
return canonical_by_norm, input_by_norm
def patch_provider_model_inputs(cfg: Dict[str, Any]) -> Tuple[int, int, int]:
"""Ensures every models.providers.*.models[].input includes text + image.
Returns: (providers_seen, models_seen, models_changed)
"""
providers_seen = 0
models_seen = 0
models_changed = 0
models = cfg.get("models")
if not isinstance(models, dict):
return providers_seen, models_seen, models_changed
providers = models.get("providers")
if not isinstance(providers, dict):
return providers_seen, models_seen, models_changed
for _provider_id, provider_cfg in providers.items():
if not isinstance(provider_cfg, dict):
continue
model_list = provider_cfg.get("models")
if not isinstance(model_list, list):
continue
providers_seen += 1
for m in model_list:
if not isinstance(m, dict):
continue
models_seen += 1
before_raw = m.get("input", None)
had_input = ("input" in m)
inputs = ensure_list_of_strings(before_raw) if had_input else []
lower = [x.lower() for x in inputs]
changed = False
if "text" not in lower:
inputs.append("text")
changed = True
if "image" not in lower:
inputs.append("image")
changed = True
inputs = dedupe_case_insensitive(inputs)
if (not had_input) or (not isinstance(before_raw, list)) or changed:
m["input"] = inputs
models_changed += 1
return providers_seen, models_seen, models_changed
def get_dict(cfg: Dict[str, Any], path: List[str]) -> Optional[Dict[str, Any]]:
cur: Any = cfg
for k in path:
if not isinstance(cur, dict):
return None
cur = cur.get(k)
return cur if isinstance(cur, dict) else None
def ensure_allowlist_contains(cfg: Dict[str, Any], canonical_model_ref: str) -> bool:
"""If agents.defaults.models exists (allowlist), ensure it includes canonical_model_ref."""
defaults = get_dict(cfg, ["agents", "defaults"])
if not isinstance(defaults, dict):
return False
allowlist = defaults.get("models")
if not isinstance(allowlist, dict):
return False
target_n = norm_ref(canonical_model_ref)
for k in list(allowlist.keys()):
if norm_ref(k) == target_n:
return False
allowlist[canonical_model_ref] = {}
return True
def pick_best_image_model(
cfg: Dict[str, Any],
canonical_by_norm: Dict[str, str],
input_by_norm: Dict[str, List[str]],
) -> Optional[str]:
"""Pick a canonical provider/model ref that supports image."""
defaults = get_dict(cfg, ["agents", "defaults"]) or {}
# 1) Prefer primary reply model (if it supports image)
model_cfg = defaults.get("model")
primary_ref = None
if isinstance(model_cfg, dict):
primary_ref = model_cfg.get("primary")
elif isinstance(model_cfg, str):
primary_ref = model_cfg
if isinstance(primary_ref, str):
n = norm_ref(primary_ref)
canonical = canonical_by_norm.get(n)
if canonical and ("image" in input_by_norm.get(n, [])):
return canonical
# 2) Try imageModel fallbacks (if any)
image_cfg = defaults.get("imageModel")
fallbacks: List[str] = []
if isinstance(image_cfg, dict):
fallbacks = [x for x in ensure_list_of_strings(image_cfg.get("fallbacks"))]
for fb in fallbacks:
n = norm_ref(fb)
canonical = canonical_by_norm.get(n)
if canonical and ("image" in input_by_norm.get(n, [])):
return canonical
# 3) First provider model with image, preserving config order
models = cfg.get("models")
if isinstance(models, dict):
providers = models.get("providers")
if isinstance(providers, dict):
for provider_key, provider_cfg in providers.items():
if not isinstance(provider_cfg, dict):
continue
model_list = provider_cfg.get("models")
if not isinstance(model_list, list):
continue
for m in model_list:
if not isinstance(m, dict):
continue
mid = m.get("id")
if not isinstance(mid, str) or not mid.strip():
continue
canonical = f"{provider_key}/{mid.strip()}"
n = norm_ref(canonical)
if "image" in input_by_norm.get(n, []):
return canonical
return None
def fix_image_model_primary(cfg: Dict[str, Any]) -> Tuple[bool, str]:
"""Ensure agents.defaults.imageModel.primary points to an existing image-capable model."""
canonical_by_norm, input_by_norm = build_provider_model_index(cfg)
defaults = get_dict(cfg, ["agents", "defaults"])
if not isinstance(defaults, dict):
return False, "SKIP: agents.defaults missing"
image_cfg = defaults.get("imageModel")
current_primary = None
if isinstance(image_cfg, dict):
current_primary = image_cfg.get("primary")
elif isinstance(image_cfg, str):
current_primary = image_cfg
image_cfg = {"primary": current_primary, "fallbacks": []}
defaults["imageModel"] = image_cfg
else:
image_cfg = {"primary": None, "fallbacks": []}
defaults["imageModel"] = image_cfg
if isinstance(current_primary, str):
n = norm_ref(current_primary)
canonical = canonical_by_norm.get(n)
if canonical and ("image" in input_by_norm.get(n, [])):
allow_changed = ensure_allowlist_contains(cfg, canonical)
if allow_changed:
return True, f"OK imageModel.primary (kept) + allowlist add: {canonical}"
return False, f"OK imageModel.primary: {canonical}"
replacement = pick_best_image_model(cfg, canonical_by_norm, input_by_norm)
if not replacement:
return False, "WARN: no provider model found that supports image; imageModel.primary not changed"
image_cfg["primary"] = replacement
allow_changed = ensure_allowlist_contains(cfg, replacement)
if allow_changed:
return True, f"SET imageModel.primary + allowlist add: {replacement}"
return True, f"SET imageModel.primary: {replacement}"
def patch_imessage_include_attachments(cfg: Dict[str, Any]) -> Tuple[bool, str]:
"""Ensure channels.imessage.includeAttachments is true (if channels.imessage exists)."""
channels = cfg.get("channels")
if not isinstance(channels, dict):
return False, "SKIP: channels missing"
im = channels.get("imessage")
if im is None:
return False, "SKIP: channels.imessage missing"
if not isinstance(im, dict):
return False, "SKIP: channels.imessage not an object"
cur = im.get("includeAttachments")
if cur is True:
return False, "OK: channels.imessage.includeAttachments already true"
im["includeAttachments"] = True
if cur is False:
return True, "SET: channels.imessage.includeAttachments false -> true"
if cur is None:
return True, "ADD: channels.imessage.includeAttachments true"
return True, f"SET: channels.imessage.includeAttachments ({type(cur).__name__}) -> true"
def main() -> int:
path = os.path.expanduser(sys.argv[1]) if len(sys.argv) > 1 else DEFAULT_PATH
if not os.path.exists(path):
print(f"ERROR: file not found: {path}", file=sys.stderr)
return 2
try:
with open(path, "r", encoding="utf-8") as f:
raw = f.read()
cfg = json.loads(raw)
except json.JSONDecodeError as e:
print(f"ERROR: JSON parse failed: {path}", file=sys.stderr)
print(f" {e}", file=sys.stderr)
return 3
if not isinstance(cfg, dict):
print("ERROR: root JSON is not an object", file=sys.stderr)
return 4
# Step 1: backup (always, overwrite)
bak_path = path + ".bak"
try:
with open(bak_path, "w", encoding="utf-8") as f:
f.write(raw)
except Exception as e:
print(f"ERROR: failed to write backup: {bak_path}: {e}", file=sys.stderr)
return 5
# Step 2: ensure provider models declare image input
providers_seen, models_seen, models_changed = patch_provider_model_inputs(cfg)
# Step 3: ensure imageModel.primary points to a real image-capable model
img_changed, img_msg = fix_image_model_primary(cfg)
# Step 4: ensure iMessage includes attachments (if configured)
im_changed, im_msg = patch_imessage_include_attachments(cfg)
try:
with open(path, "w", encoding="utf-8") as f:
json.dump(cfg, f, ensure_ascii=False, indent=2)
f.write("\n")
except Exception as e:
print(f"ERROR: failed to write patched file: {path}: {e}", file=sys.stderr)
return 6
print(f"Backup: {bak_path}")
print(f"Patched: {path}")
print(f"Providers scanned: {providers_seen}")
print(f"Models scanned: {models_seen}")
print(f"Models updated (input): {models_changed}")
print(f"Image model fix: {img_msg}")
print(f"iMessage attachments: {im_msg}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
网友解答:
--【壹】--:
感觉是中转站不支持图片
--【贰】--:
可以用cherry studio图片测试下如果可以那么openclaw也可以
--【叁】--:
Mark 一会试试
--【肆】--:
在那些llm providers验证过?好像纯文本的中转测试不行。
--【伍】--:
我还真是就默认 text
--【陆】--:
text会自动把图片base64然后识别的
--【柒】--:
可以直接运行脚本自动补全了,不过不支持图片识别的模型不知道加了会怎么样
--【捌】--:
是grok吗,那应该是上游设置了base64
image758×160 5.98 KB
--【玖】--:
gpt54
openclaw 的总结:
验证结果出来了,而且很关键:curl + base64 + xx/gpt-5.4 这条链路是成功的。
我刚刚实际跑的是:
- 模型:
gpt-5.4 - 接口:
POST https://claude.xx.vin/openai/responses - 图片:你刚发的
wechat-inbound-2286276.jpg - 图片传法:
data:image/jpeg;base64,... - 结果:流式响应完整
completed
模型最终返回的原文是:
This is the Doubao AI chat page, showing an AI-generated Chinese answer about baby crib safety and vertical slat design.
这说明两件事:
- xx 的
gpt-5.4本身能看图,没问题
- 同一个 key
- 同一个模型
- 同一张图
- 手工
curl成功识别
- 所以 OpenClaw 里失败的根因,不是 xx/gpt-5.4 不支持图片
- 而是 OpenClaw 发给 xx 的请求方式/headers/上下文,与我这条成功的 curl 不一致
- 或者 OpenClaw 在运行时还混入了别的 profile / 旧配置 / fallback 逻辑,导致 provider 侧回了 403
现在问题被缩小到很具体的范围了:
curl -> xx/gpt-5.4 + image:成功OpenClaw -> xx/gpt-5.4 + same image:403
所以接下来不该再怀疑模型能力,而应该查 OpenClaw 到 xx 的适配层。最可能是这几类差异:
baseUrl/ 路径拼接和我们 curl 不完全一样- header 附带方式不同
- OpenClaw 走的是它内部的
openai-responses适配器,但加了额外字段,被 xx 拒绝 - 仍然有旧会话/旧 provider profile 在用旧认证状态
如果你愿意,我下一步就直接去查 OpenClaw 源码里 openai-responses provider adapter,把它最终构造的请求格式对照我这条成功 curl,找出差异。这样就能把“为什么 OpenClaw 原生不行,而 curl 行”钉死。
--【拾】--:
openclaw还是得边修边补着用,离真正大伙真正的预想看似很近实则非常远~
--【拾壹】--:
好像是我写的插件的问题。AI帮我修好了,似乎能用了。坏消息是插件崩溃了,慢慢修吧
--【拾贰】--:
佬太猛了 我傻傻的自己手动写json
--【拾叁】--:
我用的3月3日的版本正常,然后就没更新过了,新版本这部分源码可能有改动过
--【拾肆】--:
写过几次老是宕机,不想写了
--【拾伍】--:
已经不想再聊了。得冷静冷静
--【拾陆】--:
Invalid type for 'input[2].content[1].image_url': expected an image URL, but got an object instead. cherry 报错,但是似乎转成base64是可以用。龙虾验证curl可行,现在是怎么让龙虾自动用的问题。论坛里没搜到研究报告
- 备份配置:读取目标配置(默认
~/.openclaw/openclaw.json,也可传入路径),并立刻写出备份openclaw.json.bak(覆盖旧备份)。 - 补齐模型图像能力声明:遍历
models.providers.*.models[],确保每个模型条目都有input字段,且包含text与image;缺什么补什么,并去重、规范化。 - 修正默认图片模型指向:检查
agents.defaults.imageModel.primary是否指向一个真实存在且input含image的provider/model;不合法就自动挑一个可用的(优先agents.defaults.model.primary,再尝试 imageModel 的 fallback,最后选第一个支持 image 的 provider 模型)。 - 处理 allowlist(如果你启用了):如果存在
agents.defaults.models(模型 allowlist),会确保上一步选中的imageModel.primary在 allowlist 里,避免“模型存在但被禁止使用”。 - iMessage 附件开关(如果你配置了 iMessage):如果存在
channels.imessage,确保includeAttachments为true(缺失就加,false 就改 true)。
执行结果:
- 会把修正后的 JSON 写回原配置文件,并打印扫描了多少 provider/model、修改了多少模型 input、imageModel 是否被修正,以及 iMessage includeAttachments 的处理结果。
测试结果: 在虚拟机中搭建测试用openclaw 直接调用脚本修改成功在openclaw网页识图
image1206×2622 446 KB
image1620×413 38.4 KB
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import json
import os
import sys
from typing import Any, Dict, List, Optional, Tuple
DEFAULT_PATH = os.path.expanduser("~/.openclaw/openclaw.json")
def ensure_list_of_strings(v: Any) -> List[str]:
if isinstance(v, list):
out: List[str] = []
for x in v:
if isinstance(x, str):
s = x.strip()
if s:
out.append(s)
return out
if isinstance(v, str):
s = v.strip()
return [s] if s else []
return []
def dedupe_case_insensitive(items: List[str]) -> List[str]:
seen = set()
out: List[str] = []
for x in items:
k = x.lower()
if k in seen:
continue
seen.add(k)
out.append(x)
return out
def norm_ref(ref: str) -> str:
return str(ref or "").strip().lower()
def build_provider_model_index(cfg: Dict[str, Any]) -> Tuple[Dict[str, str], Dict[str, List[str]]]:
"""
Returns:
- canonical_by_norm: map of "provider/model" (lowercased) -> canonical "Provider/model"
- input_by_norm: map of norm ref -> list of input strings (lowercased)
"""
canonical_by_norm: Dict[str, str] = {}
input_by_norm: Dict[str, List[str]] = {}
models = cfg.get("models")
if not isinstance(models, dict):
return canonical_by_norm, input_by_norm
providers = models.get("providers")
if not isinstance(providers, dict):
return canonical_by_norm, input_by_norm
for provider_key, provider_cfg in providers.items():
if not isinstance(provider_cfg, dict):
continue
model_list = provider_cfg.get("models")
if not isinstance(model_list, list):
continue
for m in model_list:
if not isinstance(m, dict):
continue
mid = m.get("id")
if not isinstance(mid, str) or not mid.strip():
continue
canonical = f"{provider_key}/{mid.strip()}"
n = norm_ref(canonical)
if n not in canonical_by_norm:
canonical_by_norm[n] = canonical
inp = ensure_list_of_strings(m.get("input"))
input_by_norm[n] = [x.lower() for x in inp]
return canonical_by_norm, input_by_norm
def patch_provider_model_inputs(cfg: Dict[str, Any]) -> Tuple[int, int, int]:
"""Ensures every models.providers.*.models[].input includes text + image.
Returns: (providers_seen, models_seen, models_changed)
"""
providers_seen = 0
models_seen = 0
models_changed = 0
models = cfg.get("models")
if not isinstance(models, dict):
return providers_seen, models_seen, models_changed
providers = models.get("providers")
if not isinstance(providers, dict):
return providers_seen, models_seen, models_changed
for _provider_id, provider_cfg in providers.items():
if not isinstance(provider_cfg, dict):
continue
model_list = provider_cfg.get("models")
if not isinstance(model_list, list):
continue
providers_seen += 1
for m in model_list:
if not isinstance(m, dict):
continue
models_seen += 1
before_raw = m.get("input", None)
had_input = ("input" in m)
inputs = ensure_list_of_strings(before_raw) if had_input else []
lower = [x.lower() for x in inputs]
changed = False
if "text" not in lower:
inputs.append("text")
changed = True
if "image" not in lower:
inputs.append("image")
changed = True
inputs = dedupe_case_insensitive(inputs)
if (not had_input) or (not isinstance(before_raw, list)) or changed:
m["input"] = inputs
models_changed += 1
return providers_seen, models_seen, models_changed
def get_dict(cfg: Dict[str, Any], path: List[str]) -> Optional[Dict[str, Any]]:
cur: Any = cfg
for k in path:
if not isinstance(cur, dict):
return None
cur = cur.get(k)
return cur if isinstance(cur, dict) else None
def ensure_allowlist_contains(cfg: Dict[str, Any], canonical_model_ref: str) -> bool:
"""If agents.defaults.models exists (allowlist), ensure it includes canonical_model_ref."""
defaults = get_dict(cfg, ["agents", "defaults"])
if not isinstance(defaults, dict):
return False
allowlist = defaults.get("models")
if not isinstance(allowlist, dict):
return False
target_n = norm_ref(canonical_model_ref)
for k in list(allowlist.keys()):
if norm_ref(k) == target_n:
return False
allowlist[canonical_model_ref] = {}
return True
def pick_best_image_model(
cfg: Dict[str, Any],
canonical_by_norm: Dict[str, str],
input_by_norm: Dict[str, List[str]],
) -> Optional[str]:
"""Pick a canonical provider/model ref that supports image."""
defaults = get_dict(cfg, ["agents", "defaults"]) or {}
# 1) Prefer primary reply model (if it supports image)
model_cfg = defaults.get("model")
primary_ref = None
if isinstance(model_cfg, dict):
primary_ref = model_cfg.get("primary")
elif isinstance(model_cfg, str):
primary_ref = model_cfg
if isinstance(primary_ref, str):
n = norm_ref(primary_ref)
canonical = canonical_by_norm.get(n)
if canonical and ("image" in input_by_norm.get(n, [])):
return canonical
# 2) Try imageModel fallbacks (if any)
image_cfg = defaults.get("imageModel")
fallbacks: List[str] = []
if isinstance(image_cfg, dict):
fallbacks = [x for x in ensure_list_of_strings(image_cfg.get("fallbacks"))]
for fb in fallbacks:
n = norm_ref(fb)
canonical = canonical_by_norm.get(n)
if canonical and ("image" in input_by_norm.get(n, [])):
return canonical
# 3) First provider model with image, preserving config order
models = cfg.get("models")
if isinstance(models, dict):
providers = models.get("providers")
if isinstance(providers, dict):
for provider_key, provider_cfg in providers.items():
if not isinstance(provider_cfg, dict):
continue
model_list = provider_cfg.get("models")
if not isinstance(model_list, list):
continue
for m in model_list:
if not isinstance(m, dict):
continue
mid = m.get("id")
if not isinstance(mid, str) or not mid.strip():
continue
canonical = f"{provider_key}/{mid.strip()}"
n = norm_ref(canonical)
if "image" in input_by_norm.get(n, []):
return canonical
return None
def fix_image_model_primary(cfg: Dict[str, Any]) -> Tuple[bool, str]:
"""Ensure agents.defaults.imageModel.primary points to an existing image-capable model."""
canonical_by_norm, input_by_norm = build_provider_model_index(cfg)
defaults = get_dict(cfg, ["agents", "defaults"])
if not isinstance(defaults, dict):
return False, "SKIP: agents.defaults missing"
image_cfg = defaults.get("imageModel")
current_primary = None
if isinstance(image_cfg, dict):
current_primary = image_cfg.get("primary")
elif isinstance(image_cfg, str):
current_primary = image_cfg
image_cfg = {"primary": current_primary, "fallbacks": []}
defaults["imageModel"] = image_cfg
else:
image_cfg = {"primary": None, "fallbacks": []}
defaults["imageModel"] = image_cfg
if isinstance(current_primary, str):
n = norm_ref(current_primary)
canonical = canonical_by_norm.get(n)
if canonical and ("image" in input_by_norm.get(n, [])):
allow_changed = ensure_allowlist_contains(cfg, canonical)
if allow_changed:
return True, f"OK imageModel.primary (kept) + allowlist add: {canonical}"
return False, f"OK imageModel.primary: {canonical}"
replacement = pick_best_image_model(cfg, canonical_by_norm, input_by_norm)
if not replacement:
return False, "WARN: no provider model found that supports image; imageModel.primary not changed"
image_cfg["primary"] = replacement
allow_changed = ensure_allowlist_contains(cfg, replacement)
if allow_changed:
return True, f"SET imageModel.primary + allowlist add: {replacement}"
return True, f"SET imageModel.primary: {replacement}"
def patch_imessage_include_attachments(cfg: Dict[str, Any]) -> Tuple[bool, str]:
"""Ensure channels.imessage.includeAttachments is true (if channels.imessage exists)."""
channels = cfg.get("channels")
if not isinstance(channels, dict):
return False, "SKIP: channels missing"
im = channels.get("imessage")
if im is None:
return False, "SKIP: channels.imessage missing"
if not isinstance(im, dict):
return False, "SKIP: channels.imessage not an object"
cur = im.get("includeAttachments")
if cur is True:
return False, "OK: channels.imessage.includeAttachments already true"
im["includeAttachments"] = True
if cur is False:
return True, "SET: channels.imessage.includeAttachments false -> true"
if cur is None:
return True, "ADD: channels.imessage.includeAttachments true"
return True, f"SET: channels.imessage.includeAttachments ({type(cur).__name__}) -> true"
def main() -> int:
path = os.path.expanduser(sys.argv[1]) if len(sys.argv) > 1 else DEFAULT_PATH
if not os.path.exists(path):
print(f"ERROR: file not found: {path}", file=sys.stderr)
return 2
try:
with open(path, "r", encoding="utf-8") as f:
raw = f.read()
cfg = json.loads(raw)
except json.JSONDecodeError as e:
print(f"ERROR: JSON parse failed: {path}", file=sys.stderr)
print(f" {e}", file=sys.stderr)
return 3
if not isinstance(cfg, dict):
print("ERROR: root JSON is not an object", file=sys.stderr)
return 4
# Step 1: backup (always, overwrite)
bak_path = path + ".bak"
try:
with open(bak_path, "w", encoding="utf-8") as f:
f.write(raw)
except Exception as e:
print(f"ERROR: failed to write backup: {bak_path}: {e}", file=sys.stderr)
return 5
# Step 2: ensure provider models declare image input
providers_seen, models_seen, models_changed = patch_provider_model_inputs(cfg)
# Step 3: ensure imageModel.primary points to a real image-capable model
img_changed, img_msg = fix_image_model_primary(cfg)
# Step 4: ensure iMessage includes attachments (if configured)
im_changed, im_msg = patch_imessage_include_attachments(cfg)
try:
with open(path, "w", encoding="utf-8") as f:
json.dump(cfg, f, ensure_ascii=False, indent=2)
f.write("\n")
except Exception as e:
print(f"ERROR: failed to write patched file: {path}: {e}", file=sys.stderr)
return 6
print(f"Backup: {bak_path}")
print(f"Patched: {path}")
print(f"Providers scanned: {providers_seen}")
print(f"Models scanned: {models_seen}")
print(f"Models updated (input): {models_changed}")
print(f"Image model fix: {img_msg}")
print(f"iMessage attachments: {im_msg}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
网友解答:
--【壹】--:
感觉是中转站不支持图片
--【贰】--:
可以用cherry studio图片测试下如果可以那么openclaw也可以
--【叁】--:
Mark 一会试试
--【肆】--:
在那些llm providers验证过?好像纯文本的中转测试不行。
--【伍】--:
我还真是就默认 text
--【陆】--:
text会自动把图片base64然后识别的
--【柒】--:
可以直接运行脚本自动补全了,不过不支持图片识别的模型不知道加了会怎么样
--【捌】--:
是grok吗,那应该是上游设置了base64
image758×160 5.98 KB
--【玖】--:
gpt54
openclaw 的总结:
验证结果出来了,而且很关键:curl + base64 + xx/gpt-5.4 这条链路是成功的。
我刚刚实际跑的是:
- 模型:
gpt-5.4 - 接口:
POST https://claude.xx.vin/openai/responses - 图片:你刚发的
wechat-inbound-2286276.jpg - 图片传法:
data:image/jpeg;base64,... - 结果:流式响应完整
completed
模型最终返回的原文是:
This is the Doubao AI chat page, showing an AI-generated Chinese answer about baby crib safety and vertical slat design.
这说明两件事:
- xx 的
gpt-5.4本身能看图,没问题
- 同一个 key
- 同一个模型
- 同一张图
- 手工
curl成功识别
- 所以 OpenClaw 里失败的根因,不是 xx/gpt-5.4 不支持图片
- 而是 OpenClaw 发给 xx 的请求方式/headers/上下文,与我这条成功的 curl 不一致
- 或者 OpenClaw 在运行时还混入了别的 profile / 旧配置 / fallback 逻辑,导致 provider 侧回了 403
现在问题被缩小到很具体的范围了:
curl -> xx/gpt-5.4 + image:成功OpenClaw -> xx/gpt-5.4 + same image:403
所以接下来不该再怀疑模型能力,而应该查 OpenClaw 到 xx 的适配层。最可能是这几类差异:
baseUrl/ 路径拼接和我们 curl 不完全一样- header 附带方式不同
- OpenClaw 走的是它内部的
openai-responses适配器,但加了额外字段,被 xx 拒绝 - 仍然有旧会话/旧 provider profile 在用旧认证状态
如果你愿意,我下一步就直接去查 OpenClaw 源码里 openai-responses provider adapter,把它最终构造的请求格式对照我这条成功 curl,找出差异。这样就能把“为什么 OpenClaw 原生不行,而 curl 行”钉死。
--【拾】--:
openclaw还是得边修边补着用,离真正大伙真正的预想看似很近实则非常远~
--【拾壹】--:
好像是我写的插件的问题。AI帮我修好了,似乎能用了。坏消息是插件崩溃了,慢慢修吧
--【拾贰】--:
佬太猛了 我傻傻的自己手动写json
--【拾叁】--:
我用的3月3日的版本正常,然后就没更新过了,新版本这部分源码可能有改动过
--【拾肆】--:
写过几次老是宕机,不想写了
--【拾伍】--:
已经不想再聊了。得冷静冷静
--【拾陆】--:
Invalid type for 'input[2].content[1].image_url': expected an image URL, but got an object instead. cherry 报错,但是似乎转成base64是可以用。龙虾验证curl可行,现在是怎么让龙虾自动用的问题。论坛里没搜到研究报告

