Skip to content

wire codecs: silent image loss — openai single remote-URL image dropped; responses image-only user item dropped #187

Description

@ranxianglei

来源: ranxianglei/billion-context#488 分析 multimodal payload accounting 时发现(billion-context PR #489 review)

Two silent image-loss gaps in the wire codecs (v0.0.47, dist/wire/index.js). Both drop image content from rebuilt request bodies without any error.

1. openaiToCore: single non-data-URL image gets no sidecar → dropped on re-encode

openaiToCore case "user" (dist/wire/index.js:243-257):

...imgs.length === 1 && firstParsed ? { rawOpenaiContent: imgs[0], imageMediaType: firstParsed.mediaType, imageBase64: firstParsed.base64 }
 : imgs.length > 1 ? { rawOpenaiContentParts: imgs } : {}

For a user message containing exactly ONE image whose url is a remote http(s) URL (not a data: URL): firstParsed = parseDataUrl(url) is undefined → first branch false; imgs.length > 1 false → no sidecar field at all. coreToOpenai then rebuilds content from text alone — the image is silently lost from the forwarded request. Data URLs and multi-image messages are preserved; only the single-remote-URL shape drops.

Repro: {messages:[{role:"user",content:[{type:"text",text:"t"},{type:"image_url",image_url:{url:"https://example.com/a.png"}}]}]} through openaiToCorecoreToOpenai: output content has no image part.

Suggested fix: keep the raw part whenever there is exactly one image, splitting out base64 only when it is a data URL:

...imgs.length === 1
  ? { rawOpenaiContent: imgs[0], ...(firstParsed ? { imageMediaType: firstParsed.mediaType, imageBase64: firstParsed.base64 } : {}) }
  : imgs.length > 1 ? { rawOpenaiContentParts: imgs } : {}

coreToOpenai already re-emits rawOpenaiContent verbatim when present (dist/wire/index.js:355-367), so no encode-side change is needed.

2. responsesToCore: image-only user item produces no core message → dropped from rebuilt input

responsesToCore case "message" (dist/wire/index.js:~505-531): the user branch computes effText = messageContent(item.content) where partText returns "" for non-text parts, and only pushes a core message when if (effText) is truthy. A user message item whose content is exclusively input_image parts (no input_text) yields effText === "" → no core message → coreToResponses never sees it → the item (and its images) is absent from the rebuilt input array.

Repro: {input:[{type:"message",role:"user",content:[{type:"input_image",image_url:"data:image/png;base64,AAA="}]}]} through responsesToCore → zero core messages.

Note the contrast with the Anthropic codec, which handles this correctly: anthropicToCore gives image blocks the placeholder text "[image]" plus a rawAnthropicBlock sidecar, and coreToAnthropic re-emits the raw block unconditionally — so image-only Anthropic content survives the round trip.

Fix needs to span both sides of the Responses codec: e.g. push a placeholder-text core message for image-bearing items with no text while keeping rawResponsesItem, and teach coreToResponses to re-emit the raw item verbatim for it (its current verbatim condition messageContent(raw.content) === message.text cannot match an image-only item because its content text is "").

Impact

For billion-context proxy mode (the only consumer that round-trips through these codecs): any OpenAI-chat or Responses client sending images in these shapes has them silently removed from what is forwarded upstream — the model never sees the image, with no error anywhere. A host-side size estimator counting the raw body will overcount relative to what is actually forwarded (conservative direction for fit-gates, but inconsistent with self-heal learning, which measures the rejected forwarded body).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions