☰
Claude Code 接入纯文本大模型报 API Error 400 Model only support text input:PreToolUse/PostToolUse 钩子改造与 TaoTo
2026/10/16 3:16:49 网站建设 项目流程

1. 报错现场还原:Read 一张 png 就炸出 API Error 400

先说清楚这个报错到底长什么样,以及它为什么让人抓狂。

你给 Claude Code 配了一个纯文本大模型(比如 GLM、DeepSeek 这类只吃文本的模型),跑得好好的,突然某一步它想"看一眼"生成的图片对不对,于是调用 Read 工具去读layout_7classes.png。下一秒终端红字刷出来:

API Error: 400 Model only support text input Request id: 02178...

然后整个会话就卡在那里,任务链断了。你重试,它可能又去读另一张图,又炸。你以为是模型的问题,换了个模型还是炸;你以为是网络问题,重连还是炸。

这个报错的核心检索词就是:Claude Code 接入纯文本大模型时,模型只支持文本输入(Model only support text input),一旦请求里混进图片字节,API 直接返回 400。

它适合谁看?适合所有把 Claude Code 接到非多模态模型上的开发者,尤其是做图像处理、数据可视化、前端截图验证这类任务的人——因为这类任务里,模型"想看图"的冲动特别强。

我踩过的坑是这样的:一开始我在~/.claude/CLAUDE.md里写了一行提示"当前模型不支持多模态,请不要上传图片",结果模型该读图还是读图。后来才想明白,问题根本不在模型身上。

真正把图片字节打包成多模态内容发给 API 的,是 Claude Code 本体(harness),不是模型。harness 不读 CLAUDE.md,它只认工具调用。只要模型决定调用Read xxx.png,harness 就会老老实实把图片字节塞进请求体,纯文本模型一看有图片块,直接 400。

所以 CLAUDE.md 那种"软提示"是靠模型自觉,模型一旦"手滑"就绕过去了。要硬性拦住,必须在工具调用真正执行之前,由本地代码阻断——这就是 PreToolUse 钩子的职责。下面我把整套方案拆开讲,包括三道闸的脚本、settings 配置、验证命令和排错。

2. 前置准备:把 Claude Code 的 endpoint 统一到 TaoToken 通道

在写钩子之前,先把接入层理顺。很多人 400 报错的根因其实是 endpoint 配得乱七八糟,一会儿走这个地址一会儿走那个地址,模型 ID 和实际能力对不上。我建议统一走 TaoToken 的通道,Base URL 固定,模型 ID 明确,后面排查问题时变量少。

TaoToken 官网在这里:https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 入口是 https://taotoken.net/api (这个不加 UTM)。

Claude Code 接入时,核心是三个环境变量:ANTHROPIC_BASE_URL、ANTHROPIC_AUTH_TOKEN、ANTHROPIC_MODEL。你可以直接 export,也可以写进 shell 配置文件。我习惯在~/.zshrc或~/.bashrc里写死,避免每次开终端都要重设:

export ANTHROPIC_BASE_URL="https://taotoken.net/api" export ANTHROPIC_AUTH_TOKEN="sk-你的TaoToken密钥" export ANTHROPIC_MODEL="你的纯文本模型ID"

Windows 上用 PowerShell 的话,对应写法是:

$env:ANTHROPIC_BASE_URL = "https://taotoken.net/api" $env:ANTHROPIC_AUTH_TOKEN = "sk-你的TaoToken密钥" $env:ANTHROPIC_MODEL = "你的纯文本模型ID"

密钥从哪来?去控制台建一个就行,地址是 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite ,建完在 API Keys 页面复制,页面是 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 。这两个链接建议收藏,后面换 key、查用量都用得上。

配好之后先别急着上钩子,先确认基础接入是通的。跑一个最简单的请求:

curl https://taotoken.net/api/v1/messages \ -H "x-api-key: $ANTHROPIC_AUTH_TOKEN" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d '{ "model": "'"$ANTHROPIC_MODEL"'", "max_tokens": 64, "messages": [{"role": "user", "content": "只回复两个字:通了"}] }'

如果返回里能看到正常的文本内容,说明 endpoint、key、model 三件套没问题。这一步很关键,因为后面钩子拦截失败时,你要能区分是"接入本身坏了"还是"图片混进来了"。如果这里就报 401,那是 key 的问题;如果报 model not found,那是模型 ID 写错了。把接入层的问题先清干净,再谈拦截。

顺便说一句,如果你打算长期用 Claude Code 跑编码和 Agent 任务,可以考虑 Coding Plan,入口在 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite ,比按量计费省心一些。不过这是后话,先把 400 解决掉。

3. 可复制配置:三道闸钩子脚本 + settings.json 片段

这一节是全文的核心,直接给可复制的文件。三道闸分别对应三条图片进入上下文的路径:

  • 闸①:模型调用Read读图片/PDF 文件 → PreToolUse 拦截
  • 闸②:模型调用 MCP 截图工具(如take_screenshot)→ PreToolUse 拦截
  • 闸③:某些 MCP 工具在返回值里内嵌 base64 图片 → PostToolUse 兜底拦截

三道闸缺一不可,因为图片进入上下文的路不止一条。

3.1 闸①:拦截 Read 读图片/PDF

文件路径:~/.claude/hooks/block_images.ps1

# PreToolUse hook: block Read calls on image files. # Why: the current model chain is text-only. Reading an image packs its bytes as # multimodal content into the API request, triggering # "API Error: 400 Model only support text input". # Behavior: on image extension, exit 2 to block; stderr is fed back to the model. # # NOTE: Keep this file ASCII-only. Windows PowerShell 5.1 parses BOM-less .ps1 as # ANSI/GBK, so any non-ASCII byte here would corrupt the script. $stdin = [Console]::In.ReadToEnd() if ([string]::IsNullOrWhiteSpace($stdin)) { exit 0 } try { $data = $stdin | ConvertFrom-Json } catch { exit 0 } if ($data.tool_name -ne "Read") { exit 0 } $filePath = $data.tool_input.file_path if ([string]::IsNullOrWhiteSpace($filePath)) { exit 0 } $ext = [System.IO.Path]::GetExtension($filePath) if ([string]::IsNullOrEmpty($ext)) { exit 0 } $ext = $ext.TrimStart('.').ToLowerInvariant() $imageExts = @( 'png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp', 'tiff', 'tif', 'svg', 'ico', 'heic', 'heif', 'avif' ) $docExts = @('pdf') if ($imageExts -contains $ext) { [Console]::Error.WriteLine( "Blocked by PreToolUse hook: reading image file (.${ext}) is not allowed " + "because the active model is text-only. Do not retry reading the image. " + "Get info via text instead, or ask the user to inspect the image." ) exit 2 } if ($docExts -contains $ext) { [Console]::Error.WriteLine( "Blocked by PreToolUse hook: reading PDF file (.${ext}) not allowed " + "(text-only model). Use pdftotext to extract text: " + "pdftotext `"${filePath}`" - (stdout). Do not retry Read on the PDF." ) exit 2 } exit 0

几个关键点必须说清楚。第一,Claude Code 通过 stdin 传 JSON 进来,格式是{"tool_name": "Read", "tool_input": {"file_path": "/path/to/file.png"}},用ConvertFrom-Json解析,解析失败就exit 0安全放行,绝不误伤。第二,exit 2是阻断信号,stderr 的内容会作为反馈发回给模型,所以提示信息要写清楚"别重试,改用文本方式"。第三,Windows 上这个脚本必须保持纯 ASCII,因为 PowerShell 5.1 会把无 BOM 的 UTF-8 当 GBK 解析,中文会乱码把字符串弄断,所以注释和提示都用英文。

3.2 闸②:拦截 MCP 截图工具

文件路径:~/.claude/hooks/block_screenshots.ps1

# PreToolUse hook: block MCP screenshot tools. # Why: MCP screenshot tools return image blocks in their tool RESULT. # Those images are NOT intercepted by the Read hook, and they get packed as # multimodal content into the API request, triggering API 400. # Behavior: always exit 2 (block) with an English hint. # # NOTE: Keep this file ASCII-only. [Console]::Error.WriteLine( "Blocked by PreToolUse hook: screenshot tools are disabled because the " + "active model is text-only (their image results would trigger API 400). " + "Use text-based inspection instead: take_snapshot for the accessibility " + "tree, or evaluate_script to query computed styles. Do not retry." ) exit 2

这个脚本不需要解析 stdin,因为它在 settings.json 里用matcher: "*screenshot*"精确匹配了截图工具。只要工具名里含screenshot,直接exit 2,引导模型改用take_snapshot(无障碍树)或evaluate_script(查计算样式)这类文本方式。

3.3 闸③:兜底拦截 MCP 返回的图片结果

文件路径:~/.claude/hooks/block_image_results.ps1

# PostToolUse hook: strip any MCP tool result that carries image content. # Why: safety net on top of the PreToolUse blockers. Any MCP tool that returns # an image block would otherwise inject multimodal content and trigger API 400. # Behavior: scan raw stdin JSON for base64 image markers; if found, output # JSON with decision=block so the image result is hidden from the model. # # NOTE: Keep this file ASCII-only. $stdin = [Console]::In.ReadToEnd() if ([string]::IsNullOrWhiteSpace($stdin)) { exit 0 } # Claude Code escapes inner quotes as \" in the raw stream. Unescape one level # so markers below match both raw and decoded form. $norm = $stdin -replace '\\"', '"' $markers = @( '"type":"image"', 'data:image/', 'data:image;', '"mimeType":"image', '"mimetype":"image' ) $hit = $false foreach ($m in $markers) { if ($norm.Contains($m)) { $hit = $true; break } } if (-not $hit) { exit 0 } $reason = "Blocked by PostToolUse hook: this tool returned an image block. " + "The active model is text-only, so the result was hidden to avoid API 400. " + "Rely on text results only (take_snapshot / evaluate_script)." $out = @{ hookSpecificOutput = @{ hookEventName = "PostToolUse" decision = "block" reason = $reason } } | ConvertTo-Json -Compress [Console]::Out.WriteLine($out) exit 0

这是 PostToolUse 钩子,在工具执行之后、结果返回模型之前执行。它不阻断工具本身,而是把含图片的结果藏起来(decision=block)。这里有个大坑:Claude Code 会把 JSON 内层引号转义成\",所以必须先-replace '\\"','"'反转义一层,否则"type":"image"这种标记根本匹配不到。

3.4 注册到全局 settings.json

文件路径:~/.claude/settings.json,把 hooks 字段加进去:

{ "env": { "ANTHROPIC_BASE_URL": "https://taotoken.net/api", "ANTHROPIC_MODEL": "你的纯文本模型ID" }, "includeCoAuthoredBy": false, "hooks": { "PreToolUse": [ { "matcher": "Read", "hooks": [ { "type": "command", "command": "powershell -NoProfile -NonInteractive -File \"C:\\Users\\<用户名>\\.claude\\hooks\\block_images.ps1\"" } ] }, { "matcher": "*screenshot*", "hooks": [ { "type": "command", "command": "powershell -NoProfile -NonInteractive -File \"C:\\Users\\<用户名>\\.claude\\hooks\\block_screenshots.ps1\"" } ] } ], "PostToolUse": [ { "matcher": "mcp__*", "hooks": [ { "type": "command", "command": "powershell -NoProfile -NonInteractive -File \"C:\\Users\\<用户名>\\.claude\\hooks\\block_image_results.ps1\"" } ] } ] } }

注意把<用户名>换成你实际的 Windows 用户名。matcher 的匹配规则:"Read"只拦 Read 工具,不影响 Bash/Edit/Write;"*screenshot*"通配所有含 screenshot 的工具;"mcp__*"通配所有 MCP 工具。command 用powershell -NoProfile -NonInteractive -File显式调用,不依赖执行策略。放在全局 settings 而非项目级,跨项目生效。

Linux/macOS 用户把脚本改成.sh,command 改成bash /home/you/.claude/hooks/block_images.sh即可,逻辑一样。

4. 验证请求:确认纯文本模型不再收到图片输入

配完不验证等于没配。这一节给三组测试,逐个确认三道闸都生效。

4.1 闸①测试:模拟 Read 图片

直接喂 stdin JSON 给脚本,看退出码:

echo '{"tool_name":"Read","tool_input":{"file_path":"/tmp/test.png"}}' | powershell -NoProfile -NonInteractive -File ~\.claude\hooks\block_images.ps1; echo "exit=$LASTEXITCODE"

预期:exit=2,并且 stderr 输出引导文案。再测几个对照场景:

测试场景输入预期实际
读 .png{"tool_name":"Read","tool_input":{"file_path":"/tmp/test.png"}}阻断 exit 2
读 .py{"tool_name":"Read","tool_input":{"file_path":"/home/jie/train.py"}}放行 exit 0
读 .pdf{"tool_name":"Read","tool_input":{"file_path":"/tmp/doc.pdf"}}阻断 exit 2
非 Read 工具{"tool_name":"Bash","tool_input":{"command":"ls"}}放行 exit 0

4.2 闸②测试:截图工具无条件阻断

powershell -NoProfile -NonInteractive -File ~\.claude\hooks\block_screenshots.ps1; echo "exit=$LASTEXITCODE"

预期exit=2并输出提示。

4.3 闸③测试:模拟含 base64 图片的 MCP 返回

$json = '{"tool_name":"mcp__chrome-devtools__take_screenshot","tool_input":{},"tool_response":"{\"type\":\"image\",\"data\":\"iVBORw0KGgoAAAANSUhEUg...\"}"}' echo $json | powershell -NoProfile -NonInteractive -File ~\.claude\hooks\block_image_results.ps1

预期:stdout 输出含decision=block的 JSON。

4.4 校验 settings.json 合法性

jq -e '.hooks.PreToolUse | length' ~/.claude/settings.json # 应输出 2 jq -e '.hooks.PostToolUse | length' ~/.claude/settings.json # 应输出 1

4.5 端到端验证

钩子在会话启动时加载,改完 settings.json 必须重启 Claude Code 才生效。重启后,在会话里发一句:

我在测试图片拦截钩子。请直接用 Read 工具读取 dataset/image.png,不要用任何替代方式,我要看这个工具调用本身的结果。

如果钩子生效,你会看到工具调用被阻断,模型收到"reading image file is not allowed"的反馈,然后改用文本方式。这时候 API Error 400 不再出现,说明纯文本模型确实没收到图片输入了。

想验证模型本身是否正常,可以去模型对话页面发个纯文本请求试试:https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=chat&utm_campaign=rewrite 。接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,里面有完整的参数说明。

5. 本篇常见错排查:401、local proxy failed、reading choices、OAuth

钩子配好后,报错不一定消失,因为有些错根本不是图片引起的。这一节把常见报错逐个对照。

报错一:401 Unauthorized。这跟图片无关,是 key 的问题。检查ANTHROPIC_AUTH_TOKEN是否过期、是否复制时带了空格。去 API Keys 页面重新生成一个:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 。注意 Base URL 和 Key 要配套,别一个用 A 通道的 key 去请求 B 通道的地址。

报错二:local proxy failed / connection refused。这通常是本地代理配置残留。检查环境变量里有没有HTTP_PROXY、HTTPS_PROXY指向一个已经关掉的本地端口。清掉这些变量再试:

unset HTTP_PROXY HTTPS_PROXY http_proxy https_proxy

报错三:reading choices / cannot read property 'choices'。这个报错说明你请求的 endpoint 格式和返回格式对不上。Claude Code 走的是 Anthropic Messages 格式(/v1/messages),如果你把 Base URL 指到了一个 OpenAI 兼容的/v1/chat/completions端点,返回结构里是choices而不是content,harness 解析就会炸。确认ANTHROPIC_BASE_URL指向的是 Anthropic 兼容入口。

报错四:OAuth / authentication_error。如果你之前用 Claude 官方账号登录过,本地可能残留 OAuth token,Claude Code 会优先用它而不是你的 API key。清掉登录态:

claude logout

然后重新用环境变量方式接入。

报错五:钩子不生效。三个检查点:一是 settings.json 的 JSON 语法是否合法(用jq校验);二是脚本路径是否写对,Windows 路径里的反斜杠要转义成\\;三是有没有重启 Claude Code——钩子只在会话启动时加载,改完不重启等于没改。

报错六:PostToolUse 兜底没拦住。大概率是转义问题。Claude Code 把内层引号转义成\",如果你的脚本没做-replace '\\"','"'反转义,"type":"image"就匹配不到。回去检查闸③脚本那一行。

残余局限要心里有数:用户直接拖拽/粘贴图片进对话,这条路径钩子拦不住,因为图片不经过任何工具调用,直接作为多模态内容进上下文。如果再遇到 400,大概率是历史对话里还残留图片块,执行/compact或/clear丢掉历史即可。另外模型直接引用 URL 加载图片也拦不住,同样不经过 Read 工具。

6. 长期跑编码任务:把接入和拦截固化下来

三道闸配好之后,Claude Code 接纯文本模型基本就稳了。但如果你打算长期用它跑编码、Agent 这类任务,还有两件事值得做。

第一件是把环境变量和钩子配置固化。环境变量写进 shell 配置文件,钩子脚本和 settings.json 纳入 dotfiles 管理,换机器时一键恢复。这样就不会出现"换了台电脑又炸 400"的情况。

第二件是考虑计费方式。按量计费适合偶尔用,长期高频跑 Agent 任务的话,Coding Plan 更划算,入口在 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite 。它针对编码场景做了额度优化,不用每次盯着 token 消耗。

最后留一个实用技巧:CLAUDE.md 里那句"请不要上传图片"的软提示可以保留,作为备份提醒无害,但防线以三道钩子为准。软提示靠模型自觉,钩子是本地硬拦截,两者不冲突。真正兜底的是钩子,别把希望寄托在提示词上。

维护方面,要增删拦截的扩展名,直接编辑block_images.ps1里的$imageExts或$docExts数组。要完全回退,删掉 settings.json 里的 hooks 字段,或者删掉~/.claude/hooks/目录下的脚本。改完记得重启 Claude Code。

纯文本模型读 PDF 的替代方案也顺手记一下:用pdftotext input.pdf -输出到 stdout,或者pdftotext input.pdf out.txt存成文本再用 Read 读.txt。闸①的 PDF 拦截提示里已经引导模型这么做了,你不需要额外配置。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询