Skip to content

[bug] Responses codec emits adjacently-tagged ImageSource/FileSource into image_url/file #564

Description

@mrPronin

Symptom

The Responses codec serializes the adjacently-tagged ImageSource enum directly into image_url, emitting an object where the Responses API requires a bare URL or data-URI string. The same defect applies to FileSource in input_file. Any image or file translated into the Responses format is unreadable upstream.

Cause

crates/switchyard-translation/src/codecs/responses/buffered.rs (in encode_responses_content):

ContentBlock::Image { source } => {
    blocks.push(json!({"type": "input_image", "image_url": source}));
}
...
ContentBlock::File { source } => {
    blocks.push(json!({"type": "input_file", "file": source}));
}

ImageSource and FileSource are adjacently tagged (crates/protocol/src/llm.rs):

#[serde(tag = "type", content = "data", rename_all = "snake_case")]

so the emitted item is

{"type": "input_image",
 "image_url": {"type": "url", "data": {"url": "https://…", "detail": null}}}

Expected vs. actual

  • Expected: "image_url": "https://…" — a string, as the API requires and as the decoder reads back.
  • Actual: a nested tagged object; upstream cannot read it.

⭐ The asymmetry is the clearest evidence this is unintended: all three codecs encode the same ImageSource, and only this one serializes it raw.

Codec Handling
openai_chat/buffered.rs openai_image_part(source) — builds {"url": …} explicitly ✅
anthropic/buffered.rs match source { … } — destructures ✅
responses/buffered.rs serializes the enum inline ⛔

And within the same match block in the Responses codec, Audio and Video are destructured correctly — only Image and File are not.

Why it survived the test suite

crates/switchyard-translation/tests/lossless_roundtrip.rs uses the Chat shape for the Responses fixture:

{"type": "input_image", "image_url": {"url": "https://example.test/image.png", "detail": "high"}}

decode_image_source accepts both an object and a string, so the fixture round-trips while never pinning the shape the encoder must emit. Real clients send "image_url": "data:image/png;base64,…" as a string.

Reproduction

Translate an Anthropic request carrying a base64 image to openai_responses and inspect input[0].content. The added test anthropic_image_encodes_as_responses_input_image_string fails on main and passes with the fix.

⚠ Note this is not reached on a same-format Responses→Responses route, because encode_request short-circuits to exact_preserved_request and replays the preserved body verbatim. It bites on cross-format routes.

Environment

  • Commit: 7f3b2fe9 (main); introduced in the initial commit 86020fab, so present in v0.2.0 too.

Suggested fix

Destructure both enums into the Responses wire shape, mirroring openai_image_part, and record a lossy diagnostic instead of dropping an unmappable source silently. PR: see linked pull request.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions