<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[codex + llamacpp + mmproj 的圖片輸入踩坑]]></title><description><![CDATA[<p dir="auto">今天測 codex cli + 本地 llama-server + qwen3.8-27b/mm，碰到一個奇怪的問題。mm本身的 vision 是正常的，llamacpp 也能正常處理一般的 <code>input_image</code> / <code>image_url</code>，但只要讓 codex 自己使用 <code>view_image</code> 工具查看圖片，就會在 llama-server 報錯。</p>
<p dir="auto">查了一下之後發現，問題是在 llamacpp 的 responses api compatibility layer。</p>
<p dir="auto">codex 的 <code>view_image</code> 工具執行完後，下一輪會送類似這種內容：</p>
<pre><code class="language-json">{
  "type": "function_call_output",
  "call_id": "call_xxx",
  "output": [
    {
      "type": "input_image",
      "image_url": "data:image/png;base64,..."
    }
  ]
}
</code></pre>
<p dir="auto">但 llamacpp 目前在處理 <code>function_call_output.output</code> 時，只接受：</p>
<pre><code class="language-json">{
  "type": "input_text"
}
</code></pre>
<p dir="auto">llamacpp 本身其實早就支援圖片，一般 responses api 的 <code>{"type": "input_image", "image_url": "..."}</code> 本來就會被轉成 chat completions 內部使用的 <code>{"type": "image_url", "image_url": {"url": "..."}}</code>。我現在加的 patch 大致就是把 <code>function_call_output</code> 的 <code>input_image</code> 轉成 <code>tool message</code> 的 <code>image_url</code>。</p>
<p dir="auto">目前我已經把這個修正提到 llamacpp upstream PR，現在正在等 CI / maintainer review。之前也有人回報過相同問題，只是 issue 後來因為 stale 被關掉了，所以這次被無視的可能性還是比較高的。我把 patch 貼在這裡，有需要的朋友自行 copy 到 llamacpp 的 root 目錄去執行一下，然後重新編譯就能支援 codex 的視覺能力了。</p>
<pre><code class="language-python">import re
import sys
from pathlib import Path

# source base 目錄
BASE = Path(__file__).resolve().parent
# marker
PATCH_COMMENT = "[codex-patch]"
# 元 message
ORIGINAL_THROW = "Output of tool call should be 'Input text'"
# patch 後
PATCH_THROW = "Output of tool call should be 'Input text' or 'Input image'"

PAT = re.compile(
  r'([ \t]+)if \(!chatcmpl_output\.contains\("type"\) \|\| '
  r'chatcmpl_output\.at\("type"\) != "input_text"\) \{\s*'
  r'throw std::invalid_argument\("Output of tool call should be \'Input text\'"\);\s*'
  r'\}\s*'
  r'[ \t]*chatcmpl_output\["type"\] = "text";'
)

NEW_TEMPLATE = (
  "@@0@@const std::string out_type = json_value(chatcmpl_output, \"type\", std::string());\n"
  "@@0@@if (out_type == \"input_text\") {\n"
  "@@1@@chatcmpl_output[\"type\"] = \"text\";\n"
  "@@0@@} else if (out_type == \"input_image\") {\n"
  "@@1@@// %s allow images in tool call output (multimodal tool results)\n"
  "@@1@@if (!chatcmpl_output.contains(\"image_url\")) {\n"
  "@@2@@throw std::invalid_argument(\"'image_url' is required for 'input_image' tool output\");\n"
  "@@1@@}\n"
  "@@1@@chatcmpl_output = json {\n"
  "@@2@@{\"image_url\", json {\n"
  "@@3@@{\"url\", chatcmpl_output.at(\"image_url\")}\n"
  "@@2@@}},\n"
  "@@2@@{\"type\", \"image_url\"},\n"
  "@@1@@};\n"
  "@@0@@} else {\n"
  "@@1@@throw std::invalid_argument(\"Output of tool call should be 'Input text' or 'Input image'\");\n"
  "@@0@@}"
) % PATCH_COMMENT

def build_replacement(indent: str, eol: str) -&gt; str:
  i0 = indent
  i1 = indent + "    "
  i2 = indent + "        "
  i3 = indent + "            "
  block = (NEW_TEMPLATE
            .replace("@@0@@", i0)
            .replace("@@1@@", i1)
            .replace("@@2@@", i2)
            .replace("@@3@@", i3))
  if eol != "\n":
    block = block.replace("\n", eol)
  return block

def main():
  target = Path(BASE)
  src = target / "tools" / "server" / "server-chat.cpp"
  if not src.is_file():
    print("server-chat.cpp not found at:", target)
    return 1

  raw = src.read_bytes()
  text = raw.decode("utf-8")

  # marker check
  if PATCH_THROW in text or PATCH_COMMENT in text:
    print("SKIP: already patched -&gt;", target)
    return 0

  # source check
  if not PAT.search(text):
    if ORIGINAL_THROW in text:
      print("STOP: file left UNTOUCHED. inspect manually:")
      print("      -&gt;", src)
      return 2
    print("SKIP: the restrictive block is absent. nothing to patch.")
    print("      -&gt;", target)
    return 0

  EOL = "\r\n" in text and "\r\n" or "\n"
  newtext, n = PAT.subn(lambda m: build_replacement(m.group(1), EOL), text, count=1)

  if n != 1:
    print("STOP: expected exactly one match, found", n)
    return 2

  # 備份元檔
  bak = src.with_name(src.name + ".codex.bak")
  if not bak.exists():
    bak.write_bytes(raw)

  src.write_bytes(newtext.encode("utf-8"))
  print("PATCHED :", src)
  print("Backup  :", bak)
  print("Next    : rebuild llama-server, then test a mtmd tool result.")
  return 0

if __name__ == "__main__":
  sys.exit(main())
</code></pre>
<p dir="auto">一個相性問題， codex 預設使用的是 websocket，llamacpp只支援 sse 並不支援 websocket，所以codex 在接 llama-server 時需要手動設定成使用 sse 模式。</p>
<p dir="auto">monitoring / tracing 問題，codex 自帶的 OTEL 跟 langfuse 的要求完全不匹配，普通的 thin-proxy 很難做到完全支援 langfuse 的 api protocol，需要用到社群的 extension。</p>
]]></description><link>https://lcz.me/topic/1684</link><generator>RSS for Node</generator><lastBuildDate>Tue, 15 Sep 2026 01:15:57 GMT</lastBuildDate><atom:link href="https://lcz.me/topic/1684.rss" rel="self" type="application/rss+xml"/><pubDate>Sun, 13 Sep 2026 15:12:58 GMT</pubDate><ttl>60</ttl><item><title><![CDATA[Reply to codex + llamacpp + mmproj 的圖片輸入踩坑 on Sun, 13 Sep 2026 16:03:12 GMT]]></title><description><![CDATA[<p dir="auto">方向对，根因就是 responses 兼容层在 function_call_output 里只认 input_text，把里面的 input_image 转成 tool message 的 image_url 是正解。</p>
<p dir="auto">Review 时可能被追问两点：</p>
<ul>
<li>转完之后要确认 chat template 对 tool 角色消息里的图片能正确渲染（Qwen 系模板一般支持，但 tool result 带图是比较新的用法），最好附一个最小复现和期望输出。</li>
<li>imbiplaza 提到的 webp 问题多半是解码库没编进去——llama.cpp 默认图像解码不带 webp，转 png 或自行编入 webp 支持即可，跟这个 patch 是两件事，可以拆开提。</li>
</ul>
<p dir="auto">上游如果又 stale，把 patch 挂到对应 issue 里再 @ 一下近期活跃的 maintainer，比纯 PR 更容易被看到。</p>
]]></description><link>https://lcz.me/post/17908</link><guid isPermaLink="true">https://lcz.me/post/17908</guid><dc:creator><![CDATA[Xiaote]]></dc:creator><pubDate>Sun, 13 Sep 2026 16:03:12 GMT</pubDate></item><item><title><![CDATA[Reply to codex + llamacpp + mmproj 的圖片輸入踩坑 on Sun, 13 Sep 2026 15:33:48 GMT]]></title><description><![CDATA[<p dir="auto">我沒有試過 webp，視覺能力用到的地方比較少，目前普通圖片還沒碰到問題</p>
<p dir="auto"><img src="https://upload.lcz.me/uploads/1921aaa1-a2ae-40c6-831a-c9e5603af018.png" alt="1.png" class=" img-fluid img-markdown" /><br />
<img src="https://upload.lcz.me/uploads/d32b5c50-93b0-435d-b8e3-905c8dd32ab6.png" alt="2.png" class=" img-fluid img-markdown" /><br />
<img src="https://upload.lcz.me/uploads/c1447f58-a5c7-4888-8939-9e96e6f4efcb.png" alt="3.png" class=" img-fluid img-markdown" /></p>
]]></description><link>https://lcz.me/post/17902</link><guid isPermaLink="true">https://lcz.me/post/17902</guid><dc:creator><![CDATA[tyaprin]]></dc:creator><pubDate>Sun, 13 Sep 2026 15:33:48 GMT</pubDate></item><item><title><![CDATA[Reply to codex + llamacpp + mmproj 的圖片輸入踩坑 on Sun, 13 Sep 2026 15:20:07 GMT]]></title><description><![CDATA[<p dir="auto">codex 实在不友好，但是我也是修正了。。。他还有潜在webp 不能显示的问题，我也一并修正了</p>
<p dir="auto"><img src="https://upload.lcz.me/uploads/243d7536-1a8d-447b-8d48-8fd887be118d.png" alt="Screenshot 2026-09-13 231857.png" class=" img-fluid img-markdown" /></p>
<p dir="auto"><img src="https://upload.lcz.me/uploads/dd297688-c4e8-4868-8003-d550f3a6a50b.png" alt="Screenshot 2026-09-13 231825.png" class=" img-fluid img-markdown" /></p>
]]></description><link>https://lcz.me/post/17900</link><guid isPermaLink="true">https://lcz.me/post/17900</guid><dc:creator><![CDATA[imbiplaza ASUS]]></dc:creator><pubDate>Sun, 13 Sep 2026 15:20:07 GMT</pubDate></item></channel></rss>