首页 > 教程攻略 > ai资讯 >【Claude Code-多模态实测】跨 provider 图片载荷归一化:raw base64、D

【Claude Code-多模态实测】跨 provider 图片载荷归一化:raw base64、D

来源:互联网 时间:2026-07-31 12:24:09

做跨模型网关的Vision适配,有个很容易踩的坑——以为"图片是base64"这句话就够了。实际上,这句话在不同provider眼里,含义完全不同。

【Claude Code-多模态实测】跨 provider 图片载荷归一化:raw base64、D

摘要

Anthropic要求的是raw base64放在source.data里;OpenAI风格接口习惯把base64包成data:image/...;base64,...这种Data URL;MCP的ImageContentdata + mimeType;某些视觉MCP server要的是path或URL;Files API / file_id又是另一种引用方式。它们都能表示同一张图片,但协议载荷完全不是一回事。

LiveKit issue #3867就是一个典型例子:Anthropic provider formatter把Data URL prefix直接塞进了Anthropic的source.data字段,结果返回400错误。promptfoo issue #1750则是另一个方向的错误:图片base64没有被当作Anthropic / Bedrock的image block发送,而是被当成普通text block,导致模型根本没通过视觉通道看图。

这里给出一个建议的跨provider图片载荷归一化方案:内部IR必须显式区分raw base64、Data URL、URL、file_id、local path、MCP ImageContent和provider image block;provider renderer只在最后一步生成Anthropic、OpenAI、MCP或国产模型API所需的具体格式;所有转换都要重新校验MIME、大小、hash和日志策略。

证据层级

层级来源用途
Anthropic 官方文档Vision说明 Anthropic base64 image block 的 data 是 raw base64,本身不带 Data URL prefix
OpenAI 官方文档Images and vision对照说明 OpenAI 风格视觉输入可使用 base64 Data URL
GitHub issuelivekit/agents #3867证明把 Data URL prefix 放进 Anthropic source.data 会导致 400
GitHub issuepromptfoo/promptfoo #1750证明 base64 image 如果被当 text block 发送,模型不会按图片处理且会触发 token 问题
MCP 官方规范Tools specification对照 MCP ImageContentdata + mimeType 格式

需要说明的是,LiveKit和promptfoo的issue是第三方工具链的实测案例,不代表Anthropic或OpenAI的官方行为说明;正式协议格式仍以provider文档为准。

"base64 图片"至少有三种含义

开发者常说"传base64图片",但这句话其实不够精确。

名称示例语义
raw base64/9j/4AAQSkZJRgABAQ...只有编码后的图片字节
Data URLdata:image/jpeg;base64,/9j/4AAQ...MIME + base64 放在一个 URL-like 字符串里
JSON image block{ type: "image", source: { ... } }provider-specific content part

这些形态不能随意互换。raw base64缺少MIME,需要旁路字段;Data URL自带MIME,但不是Anthropic source.data期望的值;provider image block又包含角色、source类型和字段名约束。

一个最小错误示例:

{
  "type": "image",
  "source": {
    "type": "base64",
    "media_type": "image/jpeg",
    "data": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQ..."
  }
}

对Anthropic来说,data应该是raw base64,不应该带data:image/jpeg;base64,前缀。图上看起来一样,但协议不认,这就麻烦了。

LiveKit #3867:Data URL prefix 进了 Anthropic source.data

LiveKit issue #3867的报告非常直接:Anthropic provider formatter在source.data中发送了带Data URL prefix的字符串:

"data": f"data:{img.mime_type};base64,{b64_data}"

issue中给出的修复是:

"data": b64_data

同时保留:

"media_type": img.mime_type

这个案例说明,跨provider formatter不能把OpenAI-style Data URL复用到Anthropic raw base64字段。正确的转换需要先解析Data URL:

data:image/png;base64,iVBORw0...
  -> mimeType = image/png
  -> rawBase64 = iVBORw0...
  -> Anthropic source.data = rawBase64
  -> Anthropic source.media_type = mimeType

而不是直接字符串搬运。

promptfoo #1750:base64 image 被当 text block

promptfoo issue #1750报告的是另一个方向的问题:在Anthropic / Bedrock vision eval中,encoded image base64 string被当作text传给Claude,而不是作为image content block。小图可能偶然得到看似合理的描述,但较大图片会因为文本token限制或模型并非为读取base64文本而设计而失败。

这个和LiveKit #3867组合起来,正好覆盖了两个常见错误:

错误表现后果
Data URL 放进 raw base64 字段Anthropic 400请求格式错误
raw base64 放进 text block模型读文本而不是看图token 膨胀、理解错误

前者是"包装太多",后者是"包装太少"。正确做法是让图片进入目标provider规定的图片通道。

内部 IR 必须 provider-neutral

不要把Anthropic image block、OpenAI content part或MCP ImageContent作为系统内部唯一表示。它们都是边界格式。

推荐IR设计:

type ImageRef =
  | {
      kind: "local_path";
      path: string;
    }
  | {
      kind: "url";
      url: string;
    }
  | {
      kind: "raw_base64";
      data: string;
      mimeType: ImageMime;
    }
  | {
      kind: "data_url";
      value: string;
    }
  | {
      kind: "file_id";
      provider: "anthropic" | "openai" | "custom";
      id: string;
    }
  | {
      kind: "artifact_ref";
      uri: string;
      mimeType: ImageMime;
    }
  | {
      kind: "mcp_image_content";
      data: string;
      mimeType: ImageMime;
    };

provider block只在renderer里生成:

type RenderTarget =
  | "anthropic_image_block"
  | "openai_image_content_part"
  | "mcp_image_content"
  | "mcp_path_argument"
  | "glm_image_path_argument";

这样IR保留语义,renderer负责最后一公里。分工明确,边界清晰。

转换矩阵

输入AnthropicOpenAI-styleMCP ImageContentpath-first MCP
local path读取 + raw base64 / file_id上传 / data URL / URL读取 + data + mimeType直传 path
URLURL sourceURL / image_url可转 resource link直传 URL
raw base64source.data包成 data URL 或 provider blockdata + mimeType写临时文件或拒绝
Data URL解包成 raw base64 + MIME可直用解包成 data + mimeType写临时文件或拒绝
file_idsource.file_idprovider file id不通用不通用
artifact ref读取或生成 file_id / URL读取或生成 URLresource link / ImageContentpath / URL

矩阵里的每个转换都要有策略约束:

约束例子
大小raw base64 超过阈值改用 file_id / URL
MIME解包或读取后用 magic bytes 校验
权限local path 不能给远程 server
日志不记录完整 base64 / signed URL
生命周期临时文件和 signed URL 要过期

Anthropic renderer

Anthropic renderer的核心规则很明确:

输入输出
raw base64source.type=base64data=rawBase64media_type=mimeType
Data URL先解析,去掉 prefix,再按 raw base64 输出
URLsource.type=url
file_idsource.type=file,引用 file_id
local path读取后 base64,或先上传 Files API

示例:

function renderAnthropicImage(ref: NormalizedImage) {
  if (ref.kind === "raw_base64") {
    return {
      type: "image",
      source: {
        type: "base64",
        media_type: ref.mimeType,
        data: ref.data
      }
    };
  }

  if (ref.kind === "url") {
    return {
      type: "image",
      source: {
        type: "url",
        url: ref.url
      }
    };
  }

  if (ref.kind === "file_id" && ref.provider === "anthropic") {
    return {
      type: "image",
      source: {
        type: "file",
        file_id: ref.id
      }
    };
  }

  throw new Error("image must be normalized before Anthropic render");
}

必须禁止:

source.data startsWith "data:image/"

也必须禁止:

content block type = "text", text = rawBase64

OpenAI-style renderer

OpenAI风格接口常见做法是把图片放进image_urlinput_image,URL可以是真实URL,也可以是Data URL。具体字段随API版本和SDK变化,但对适配层来说,核心只有几点:

输入OpenAI-style 输出
URL作为 image URL
raw base64 + MIME包成 data:${mimeType};base64,${data}
Data URL可直接作为 image URL,但仍应校验 MIME
file_id如果该 API 支持 file id,则走文件引用

示例:

function toDataUrl(image: { mimeType: string; data: string }) {
  return `data:${image.mimeType};base64,${image.data}`;
}

关键提醒:不要把OpenAI-style Data URL回传给Anthropic renderer。Data URL是某些provider的目标格式,不是内部通用格式。

MCP renderer

MCP ImageContent和Anthropic image block很像,但字段不同:

MCPAnthropic
type: "image"type: "image"
datasource.data
mimeTypesource.media_type
source.typesource.type: "base64"

转换看似简单,但仍要注意:

function renderMcpImageContent(ref: NormalizedImage) {
  if (ref.kind !== "raw_base64") {
    throw new Error("MCP ImageContent requires raw base64 data");
  }

  return {
    type: "image",
    data: ref.data,
    mimeType: ref.mimeType
  };
}

如果图片太大,不应强行返回ImageContent,应返回resource link / artifact ref。否则就会踩到前文讨论的tool result token成本问题。

path / URL 型 MCP renderer

对Z.ai / GLM这类path-first或URL-first视觉MCP server,renderer反而不应生成image block:

{
  "image_path": "/workspace/demo.png",
  "prompt": "What error is shown?"
}

或:

{
  "image_path": "https://signed.example.com/demo.png",
  "prompt": "What error is shown?"
}

如果输入是raw base64,需要先根据策略落盘或生成URL:

raw_base64
  -> decode
  -> validate MIME
  -> write to $tmp/vision/img.png
  -> pass path

如果MCP server是远程服务,不能传本地path,应生成scoped URL。

归一化管线

推荐统一管线:

accept input
  -> classify: path / URL / raw base64 / Data URL / ImageContent / file_id
  -> inspect: MIME, bytes, dimensions, hash
  -> normalize: strip Data URL prefix, decode, re-encode, upload, artifactize
  -> route: choose target capability
  -> render: provider-specific payload
  -> validate: no wrong wrapper, no base64-as-text

关键是classify必须早于render。不要等到renderer里再猜字符串是什么。

分类规则:

输入特征分类
data:image/...;base64,Data URL
http:// / https://URL
本地存在文件path
解码后 magic bytes 是图片raw base64
{ type: "image", source: ... }provider image block
{ type: "image", data, mimeType }MCP ImageContent

对字符串base64要谨慎:随机token、ID、长文本都可能看起来像base64。必须解码并检查magic bytes。

日志和审计

图片载荷归一化很容易泄露数据。日志策略应区分对待:

字段日志策略
raw base64不记录
Data URL不记录完整值
URL脱敏 query / token
local path脱敏 home / 用户名
file_id可记录 provider + id hash
artifact_ref可记录,但需 ACL
hash推荐记录
MIME / bytes / dimensions推荐记录

转换provenance示例:

{
  "inputKind": "data_url",
  "outputKind": "anthropic_image_block",
  "conversion": "strip_data_url_prefix",
  "mimeType": "image/png",
  "bytes": 184320,
  "hash": "sha256:...",
  "target": "anthropic_messages"
}

这样LiveKit #3867这类问题可以直接在日志里定位到"Data URL没有strip prefix"。

错误信息

错误要明确指出包装层级:

{
  "error": "invalid_anthropic_base64_payload",
  "reason": "source.data contains a Data URL prefix",
  "actualPrefix": "data:image/jpeg;base64,",
  "expected": "raw base64 only",
  "suggestedFix": "strip the data URL prefix and move MIME into source.media_type"
}

base64-as-text:

{
  "error": "image_payload_sent_as_text",
  "reason": "base64 image data was placed in a text block",
  "expected": "provider-native image block",
  "impact": "model will not receive a visual input and token cost may explode"
}

这两种错误要分开。一个是字段值包装错,一个是content block类型错。不能混为一谈。

Contract tests

至少需要覆盖这些测试:

测试断言
Data URL -> Anthropicstrip prefix,source.data 是 raw base64
raw base64 -> Anthropic不添加 Data URL prefix
raw base64 -> OpenAI-style生成 Data URL
Data URL -> MCP ImageContent解包为 data + mimeType
raw base64 text block拒绝或转 image block
URL -> Anthropic使用 URL source,不下载成 base64,除非策略要求
path -> path-first MCP保留 path,不转 provider block
oversized base64转 file_id / artifact / URL
unsupported MIME拒绝或转码

针对LiveKit #3867:

Given: input is Data URL data:image/jpeg;base64,/9j...
When: rendering Anthropic image block
Then: source.data == "/9j..."
And: source.media_type == "image/jpeg"
And: source.data does not start with "data:"

针对promptfoo #1750:

Given: input is raw base64 image
When: rendering Anthropic message
Then: content block type == "image"
And: no text block contains the base64 payload

这两个测试用例,建议直接写进CI里。

工程结论

跨provider Vision适配的基本功,是把图片载荷表示说清楚。raw base64、Data URL、URL、file_id、path、MCP ImageContent和provider image block都不是同一种东西。它们之间可以转换,但不能混用。

LiveKit #3867说明,OpenAI-style Data URL prefix放进Anthropic source.data会导致请求失败;promptfoo #1750说明,base64如果进入text block,模型就不是在看图,而是在读一大段文本。这两个问题一个是包装过度,一个是包装不足,本质都是没有provider-neutral IR和严格renderer。

可靠的适配层应该先classify,再inspect,再normalize,最后按target capability render。renderer要明确禁止Data URL进入Anthropic raw base64字段,也要禁止图片base64进入普通text block。这样图片才能在Anthropic、OpenAI-compatible、MCP server、国产模型和企业gateway之间稳定流转。

参考链接

• Anthropic Vision
• OpenAI Images and vision
• livekit/agents Issue #3867
• promptfoo/promptfoo Issue #1750
• MCP Tools specification