Skip to content

fix(aws): preserve Bedrock image format#1974

Open
rosetta-livekit-bot[bot] wants to merge 1 commit into
mainfrom
dreaming-villas-queerest
Open

fix(aws): preserve Bedrock image format#1974
rosetta-livekit-bot[bot] wants to merge 1 commit into
mainfrom
dreaming-villas-queerest

Conversation

@rosetta-livekit-bot

@rosetta-livekit-bot rosetta-livekit-bot Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add AWS Bedrock chat context provider formatting in agents-js
  • preserve serialized image MIME format when building Bedrock image payloads
  • reject external URL images for Bedrock and add equivalent upstream regression coverage

Tests

  • pnpm test -- agents/src/llm/provider_format/aws.test.ts
  • pnpm --filter @livekit/agents typecheck
  • pnpm --filter @livekit/agents build
  • pnpm --filter @livekit/agents lint (passes with existing warnings)

Notes

  • pnpm --filter @livekit/agents api:check currently fails on existing agents/dist/index.d.ts export namespace syntax, unrelated to this change.

Ported from livekit/agents#6328

Original PR description

Fixes AWS Bedrock chat-context formatting for non-JPEG ImageContent inputs by deriving the Bedrock image block format from the serialized image MIME type instead of always declaring jpeg.

What Problem This Solves

AWS Bedrock image blocks require the declared format to match the image bytes being sent.

LiveKit already preserves that information when it serializes an ImageContent data URL. For example, a PNG data URL is decoded into raw PNG bytes with mime_type="image/png":

SerializedImage(
    data_bytes=...,        # original PNG bytes
    mime_type="image/png",
)

The bug is in the AWS formatter. It receives the serialized image, but always declares the Bedrock image format as jpeg:

{
    "image": {
        "format": "jpeg",
        "source": {"bytes": img.data_bytes},
    }
}

That is correct only when the serialized image is actually JPEG. For PNG, GIF, or WebP data URLs, the formatter keeps the original non-JPEG bytes but labels them as JPEG in the Bedrock request.

So the problem is not that LiveKit loses or rewrites the image bytes. The problem is that the AWS request metadata no longer matches those bytes.

AWS Bedrock supports png, jpeg, gif, and webp as ImageBlock.format values, so the formatter should use the serialized MIME type to choose the matching Bedrock format.

Change

Map SerializedImage.mime_type to the corresponding Bedrock image format:

image/jpeg -> jpeg
image/png  -> png
image/gif  -> gif
image/webp -> webp

This keeps the existing JPEG behavior for VideoFrame inputs, because serialize_image() currently encodes frames as JPEG.

This also keeps the current Bedrock behavior for external image URLs unchanged: external URLs are still rejected because this formatter path only builds inline Bedrock image blocks.

Evidence

A PNG ImageContent can currently be serialized with the correct MIME type but formatted for AWS with the wrong Bedrock format:

from livekit.agents.llm import ChatContext, ImageContent

ctx = ChatContext.empty()
ctx.add_message(
    role="user",
    content=[ImageContent(image="data:image/png;base64,<png-bytes>")],
)

messages, _ = ctx.to_provider_format(format="aws")

Before this fix, the generated Bedrock message declares the image as JPEG:

{
    "image": {
        "format": "jpeg",
        "source": {"bytes": b"<original png bytes>"},
    }
}

After this fix, the declared format matches the serialized image bytes:

{
    "image": {
        "format": "png",
        "source": {"bytes": b"<original png bytes>"},
    }
}

The same mapping applies to JPEG, GIF, and WebP inputs.

AWS documents ImageBlock.format valid values as png | jpeg | gif | webp: https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_ImageBlock.html

Possible call chain / impact

livekit.agents.llm.ChatContext
  -> ChatContext.to_provider_format(format="aws")
    -> livekit.agents.llm._provider_format.aws.to_chat_ctx(...)
      -> _build_image(...)
        -> llm.utils.serialize_image(...)
        -> Bedrock image block

The affected path is only AWS Bedrock chat-context conversion for inline image inputs.

This PR does not change:

  • image serialization itself
  • VideoFrame encoding behavior
  • external URL handling for AWS Bedrock
  • OpenAI, Anthropic, Google, or Mistral provider formatting
  • tool/function-call formatting

Sibling provider surfaces already preserve image MIME type differently: OpenAI and Mistral include the MIME type in data URLs, Anthropic passes media_type, and Google passes mime_type in image parts. AWS was the outlier because it dropped the MIME type and hardcoded the Bedrock format.

Validation

  • uv run --no-sync pytest tests/test_chat_ctx.py -q - passed, 27 passed / 1 skipped
  • uv run --no-sync pytest tests/test_chat_ctx.py -q -k aws_image_content - passed, 5 passed
  • uv run --no-sync ruff check livekit-agents/livekit/agents/llm/_provider_format/aws.py tests/test_chat_ctx.py - passed
  • uv run --no-sync ruff format --check livekit-agents/livekit/agents/llm/_provider_format/aws.py tests/test_chat_ctx.py - passed
  • git diff --check - passed

@rosetta-livekit-bot rosetta-livekit-bot Bot requested a review from a team as a code owner July 6, 2026 22:42
@changeset-bot

changeset-bot Bot commented Jul 6, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: b2a64b0

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 36 packages
Name Type
@livekit/agents Patch
@livekit/agents-plugin-anam Patch
@livekit/agents-plugin-anthropic Patch
@livekit/agents-plugin-assemblyai Patch
@livekit/agents-plugin-baseten Patch
@livekit/agents-plugin-bey Patch
@livekit/agents-plugin-cartesia Patch
@livekit/agents-plugin-cerebras Patch
@livekit/agents-plugin-deepgram Patch
@livekit/agents-plugin-did Patch
@livekit/agents-plugin-elevenlabs Patch
@livekit/agents-plugin-fishaudio Patch
@livekit/agents-plugin-google Patch
@livekit/agents-plugin-hedra Patch
@livekit/agents-plugin-hume Patch
@livekit/agents-plugin-inworld Patch
@livekit/agents-plugin-lemonslice Patch
@livekit/agents-plugin-liveavatar Patch
@livekit/agents-plugin-livekit Patch
@livekit/agents-plugin-minimax Patch
@livekit/agents-plugin-mistral Patch
@livekit/agents-plugin-mistralai Patch
@livekit/agents-plugin-neuphonic Patch
@livekit/agents-plugin-openai Patch
@livekit/agents-plugin-perplexity Patch
@livekit/agents-plugin-phonic Patch
@livekit/agents-plugin-resemble Patch
@livekit/agents-plugin-rime Patch
@livekit/agents-plugin-runway Patch
@livekit/agents-plugin-sarvam Patch
@livekit/agents-plugin-silero Patch
@livekit/agents-plugin-soniox Patch
@livekit/agents-plugin-tavus Patch
@livekit/agents-plugins-test Patch
@livekit/agents-plugin-trugen Patch
@livekit/agents-plugin-xai Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 potential issues.

Open in Devin Review

toolResult: {
toolUseId: msg.callId,
content: [{ text: msg.output }],
status: 'success',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Tool error results are always reported as successful to the AI model

Tool call results are always marked as successful (status: 'success' at agents/src/llm/provider_format/aws.ts:84) regardless of whether the tool actually failed, so the AI model cannot distinguish between successful and failed tool executions.

Impact: When a tool call fails, the AI model will treat the error message as a valid result, potentially producing incorrect or misleading responses.

Mechanism: isError field on FunctionCallOutput is ignored

The FunctionCallOutput type has an isError: boolean field (agents/src/llm/chat_context.ts:539). Other provider format adapters properly check this field — for example, the Google adapter at agents/src/llm/provider_format/google.ts:87 uses msg.isError ? { error: msg.output } : { output: msg.output }. The AWS Bedrock Converse API supports status: 'error' for toolResult blocks, but the AWS adapter hardcodes status: 'success' at line 84 without checking msg.isError.

Suggested change
status: 'success',
status: msg.isError ? 'error' : 'success',
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +94 to +96
if (injectDummyUserMessage && (messages.length === 0 || messages[0]?.role !== 'user')) {
messages.unshift({ role: 'user', content: [{ text: '(empty)' }] });
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Dummy user message injection targets the first message, unlike Google which targets the last

The injectDummyUserMessage parameter is used differently across providers. In agents/src/llm/provider_format/aws.ts:94-96, a dummy user message is prepended (unshift) to ensure the first message is from the user. In contrast, agents/src/llm/provider_format/google.ts:104-106 appends a dummy user message to ensure the last message is from the user. Both behaviors are valid for their respective APIs (Bedrock requires user-first, Gemini requires user-last), but the Bedrock Converse API also requires the last message to be from the user role. If the conversation ends with an assistant message, the current implementation won't add a trailing user message, which could cause API errors. This may be handled by the calling plugin, but it's worth verifying.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants