Skip to content

Inbound images silently fail: replayed/history image attachments sent to Anthropic with non-ASCII source.base64 #86984

@edward-lcl

Description

@edward-lcl

Agent silently fails on inbound images: replayed image attachments sent to Anthropic with non-base64 source.base64

Version: 2026.5.24-beta.2 (abb43c9) (Homebrew install, macOS)
Channels seen: Discord (also reproduces on the OpenAI/Responses failover path)

Symptom

When a user sends an image in a channel, the agent often produces no reply at all. The gateway run ends with isError=true and the Anthropic API rejects the entire turn:

embedded run agent end: isError=true model=claude-sonnet-4-6 provider=anthropic
error=LLM request rejected:
  messages.337.content.1.image.source.base64: string argument should contain only ASCII characters

On the OpenAI/Responses failover the same underlying data triggers:

Invalid 'input[0].output[1].image_url'. Expected a base64-encoded data URL ...
but got an invalid base64-encoded value.

Root cause

The inbound image file on disk is valid and base64-encodes to pure ASCII. The corruption happens during per-turn message assembly: a replayed/inline image attachment reaches the provider mapper with its data field holding a raw (latin1/binary) byte string instead of base64.

  • The current-turn path is correct — resolveAgentTurnAttachments in agent-turn-attachments reads the buffer and does data: buffer.toString("base64").
  • The inline/replayed-history path forwards data untouched:
    • resolveInlineAgentImageAttachments{ mediaType: image.mimeType, data: image.data }
    • The Anthropic provider mapper (provider-stream) then builds, for every block:
      source: { type: "base64", media_type: block.mimeType, data: block.data }

If data is unencoded bytes, source.base64 contains non-ASCII and the API rejects the whole request — so a single bad historical image block poisons every subsequent turn in a long-lived session (images are stored as path-refs and re-hydrated each turn).

This looks like the counterpart to #83466 ("hydrate current inbound image attachments"), which fixed the current attachment path but not the replayed/inline one.

Reproduction

  1. Send an image to the agent in a Discord channel.
  2. Continue the conversation so the image becomes a history entry in a long-lived session.
  3. Send any follow-up message — the turn is rejected with the image.source.base64 ... only ASCII characters error and the agent goes silent.

Suggested fix

Guarantee data is ASCII base64 at the point it becomes source.base64. A safe, idempotent guard (valid base64 is always ASCII, so correct payloads pass through untouched):

const toAsciiBase64 = (d) =>
  (typeof d === "string" && !/^[A-Za-z0-9+/=\s]*$/.test(d))
    ? Buffer.from(d, "latin1").toString("base64")
    : d;

Better still, fix the source so the inline/replayed-history hydration base64-encodes like the current-turn path does. The same raw-data passthrough also exists on the OpenAI/Responses mappers and should get the same treatment.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions