Skip to content

fix(bedrock): developer-role, empty-content, and streaming terminal events#3188

Open
amarcin wants to merge 3 commits into
maximhq:mainfrom
amarcin:fix/bedrock-developer-role
Open

fix(bedrock): developer-role, empty-content, and streaming terminal events#3188
amarcin wants to merge 3 commits into
maximhq:mainfrom
amarcin:fix/bedrock-developer-role

Conversation

@amarcin
Copy link
Copy Markdown

@amarcin amarcin commented May 2, 2026

Summary

Three related defects in the Bedrock Converse translator that together blocked OpenAI Codex (CLI 0.128.0 and Desktop 0.128.0-alpha.1) from completing a single turn against Bedrock via Bifrost. All three live in core/providers/bedrock/{responses.go,utils.go}, all three were discovered in one integration pass, and all three are verified together on v1.4.24 deployed live.

Keeping them bundled because (a) they're the same class — assumptions about OpenAI's wire format that don't hold for every client, (b) scope is identical (two files, both Bedrock-only), (c) they compound: fixing any one in isolation doesn't unblock Codex.

Changes

1. developer role not mapped to system (fixes #2492)

OpenAI's Chat Completions and Responses APIs accept role: "developer" as a system-message variant (introduced with GPT-5 / reasoning models). Bedrock Converse rejects anything other than user / assistant in its messages array.

Value 'developer' at 'messages.N.member.role' failed to satisfy constraint:
Member must satisfy enum value set: [user, assistant]

The responses-to-chat fallback in core/schemas/mux.go already handles this via normalizeDeveloperRoleForChatFallback, and the Gemini provider handles it at core/providers/gemini/responses.go:2962. The native Bedrock translators just missed the case.

  • core/providers/bedrock/responses.go: fold ResponsesInputMessageRoleDeveloper into the system-message branch
  • core/providers/bedrock/utils.go: same for ChatMessageRoleDeveloper

2. Empty-content messages forwarded to Bedrock

Codex emits placeholder assistant messages with content: [] between tool-call turns. The translator forwarded these as Bedrock messages with empty Content slices, which Bedrock rejects:

Value null at 'messages.N.member.content' failed to satisfy constraint: Member must not be null
  • convertBifrostMessageToBedrockMessage returns nil when converted content is empty (caller already handles nil by skipping)
  • convertMessages skips user/assistant messages whose converted content is empty

3. Streaming terminal events dropped accumulated text

The Bedrock responses-stream translator emits per-token response.output_text.delta events correctly, but hardcoded emptyText := "" on every terminal event:

  • response.output_text.donetext=""
  • response.content_part.donepart.text=""
  • response.output_item.doneitem.content=[]
  • response.completedresponse.output=null

Clients that accumulate deltas themselves (OpenAI SDK, Codex CLI) were unaffected. Clients that rehydrate from terminal events (Codex Desktop) rendered empty bubbles because the authoritative post-stream state said the message had no content.

  • Adds TextBuffers map[int]string to BedrockResponsesStreamState, populated from both text-delta emit sites
  • Terminal event emitters now use accumulated text instead of a hardcoded empty string
  • response.completed reconstructs plain-text items in response.Output from the buffer (tool-call items are materialised separately during the stream and are a pre-existing out-of-scope limitation)

Diff size

  • core/providers/bedrock/responses.go: +85 / -7
  • core/providers/bedrock/utils.go: +10 / -2

How to test

# Fix 1: developer role
curl https://<bifrost>/v1/responses \
  -H 'Authorization: Bearer <vk>' -H 'Content-Type: application/json' \
  -d '{
    "model":"bedrock/us.anthropic.claude-opus-4-7",
    "input":[
      {"role":"developer","content":"Speak like a pirate."},
      {"role":"user","content":"Hi"}
    ],
    "max_output_tokens":40
  }'
# Before: HTTP 400 (developer rejected). After: 200 with pirate reply.

# Fix 2: empty assistant content in history
curl https://<bifrost>/v1/responses \
  -H 'Authorization: Bearer <vk>' -H 'Content-Type: application/json' \
  -d '{
    "model":"bedrock/us.anthropic.claude-opus-4-7",
    "input":[
      {"role":"user","content":"hi"},
      {"role":"assistant","content":[]},
      {"role":"user","content":"say pong"}
    ],
    "max_output_tokens":10
  }'
# Before: HTTP 400 (null content). After: 200.

# Fix 3: streaming terminal events
curl -N https://<bifrost>/v1/responses \
  -H 'Authorization: Bearer <vk>' -H 'Content-Type: application/json' \
  -d '{
    "model":"bedrock/us.anthropic.claude-opus-4-7",
    "input":[{"role":"user","content":"say pong"}],
    "max_output_tokens":10, "stream":true
  }'
# Inspect the response.output_text.done / response.completed events.
# Before: text="" / output=null. After: text="pong" / output=[{...content:[{text:"pong"}]...}].

End-to-end: Codex Desktop 0.128.0-alpha.1 → Bifrost → Bedrock Opus 4.7 (us-east-1) rendered empty bubbles before; full multi-turn with tool use after.

Type of change

  • Bug fix

Affected areas

  • Providers/Integrations (Bedrock)

Breaking changes

  • No

Each fix strictly expands the set of inputs the translator accepts or enriches the set of outputs it emits. No existing working call pattern changes shape.

Related issues

Fixes #2492. (#2528 was an earlier attempt at #1 that closed without merging — this PR takes the simpler, single-line approach the Gemini provider and chat fallback already use.)

OpenAI's Chat Completions and Responses APIs permit role="developer" as
a system-message variant (introduced for GPT-5 / reasoning models).
Bedrock Converse rejects anything other than user/assistant in the
messages array, so passing "developer" through verbatim produces:

  Value 'developer' at 'messages.N.member.role' failed to satisfy
  constraint: Member must satisfy enum value set: [user, assistant]

Fold "developer" into the system-message branch in both code paths so
the message text lands in Converse's system field instead. This matches
what normalizeDeveloperRoleForChatFallback already does for the
responses-to-chat fallback path and what the Gemini provider does in
core/providers/gemini/responses.go:2962.

Fixes maximhq#2492
@CLAassistant
Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 2, 2026

📝 Walkthrough

Walkthrough

Streaming response conversion now buffers assistant text per output index and emits finalized text only at stream finalization; message conversion treats role "developer" as "system" and drops converted Bedrock messages with empty content blocks to avoid sending blank turns.

Changes

Streaming response buffering & finalization

Layer / File(s) Summary
State Shape
core/providers/bedrock/responses.go
Add BedrockResponsesStreamState.TextBuffers map[int]string to accumulate assistant text per outputIndex.
Pool Initialization / Reset
core/providers/bedrock/responses.go
Initialize and clear TextBuffers in the bedrockResponsesStreamStatePool acquire/flush logic.
Streaming Accumulation
core/providers/bedrock/responses.go
During streaming, append incoming assistant text to state.TextBuffers[outputIndex] instead of emitting incremental output_text.delta events.
Finalization / Emission
core/providers/bedrock/responses.go
FinalizeBedrockStream emits output_text.done and content_part.done with the accumulated text from TextBuffers and reconstructs plain-text response.Output items from buffered content up to CurrentOutputIndex.
Tests / Docs (implicit)
...
No explicit test or docs files changed in this diff (behavioral changes concentrated in responses.go).

Role folding and empty-content guard

Layer / File(s) Summary
Role Dispatch
core/providers/bedrock/utils.go, core/providers/bedrock/responses.go
Treat schemas.ChatMessageRoleDeveloper the same as system during conversion so developer messages follow the system-message path.
Conversion Guard
core/providers/bedrock/utils.go, core/providers/bedrock/responses.go
When converting user/assistant messages, skip or return nil for Bedrock messages whose Content/contentBlocks are empty to avoid sending empty-content turns to Bedrock Converse.
Comments / Inline Docs
core/providers/bedrock/utils.go
Update comment describing folding of developer into system for Bedrock compatibility.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

"I nibble bytes and buffer dreams,
Texts I stash in gentle streams.
Developer folds into system's light,
Empty echoes vanish from sight.
Hop, hop—stream final, all is right! 🥕"

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Out of Scope Changes check ❓ Inconclusive The PR includes additional commits addressing streaming response text accumulation and empty content filtering for Bedrock Converse, which are related defects discovered during Codex integration testing but were not explicitly in the scope of issue #2492. Clarify whether the streaming text accumulation and empty-content-filtering commits should be in this PR or moved to a separate issue/PR focused on those specific defects.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The code changes directly address issue #2492 by mapping developer role to system messages in Bedrock's chat and responses translators, normalizing rejected role validation errors [#2492].
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Title check ✅ Passed The title accurately summarizes all three main fixes: developer-role mapping, empty-content message handling, and streaming terminal event text accumulation.
Description check ✅ Passed The description is comprehensive, covering all required sections with detailed problem statements, solutions, testing steps, and clear justification for bundling the three related fixes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 2, 2026

Confidence Score: 5/5

Safe to merge; the core developer-role fix is correct and the additional streaming improvements are well-guarded.

Only P2 findings present. The developer-role mapping is straightforward and consistent with the Gemini provider and mux fallback. Nil returns from convertBifrostMessageToBedrockMessage are properly guarded at the call site. The incomplete tool-call population in response.completed is a pre-existing limitation explicitly acknowledged in comments, not a regression.

core/providers/bedrock/responses.go — the response.completed Output loop is text-only; tool-call items remain absent from that event.

Important Files Changed

Filename Overview
core/providers/bedrock/responses.go Folds ResponsesInputMessageRoleDeveloper into the system-message branch; adds TextBuffers to accumulate streamed text so output_text.done/output_item.done carry the full body; populates response.Output text items for response.completed; drops messages with empty content blocks.
core/providers/bedrock/utils.go Adds ChatMessageRoleDeveloper to the system-message case in convertMessages; skips user/assistant messages that convert to zero content blocks.

Reviews (3): Last reviewed commit: "fix(bedrock): accumulate streamed text f..." | Re-trigger Greptile

coderabbitai[bot]
coderabbitai Bot previously approved these changes May 2, 2026
…nses

Codex CLI emits placeholder assistant messages with content=[] between
tool-call turns, and historic transcripts may contain user/assistant
messages whose content consisted entirely of reasoning/metadata blocks
that this converter does not map to Bedrock. In either case, the
current code produces a BedrockMessage with empty/null Content, which
Bedrock Converse rejects:

  Value null at 'messages.N.member.content' failed to satisfy
  constraint: Member must not be null

Skip such messages in both the chat completions and responses
translators so upstream callers receive a coherent message sequence.

Part of maximhq#2492
@amarcin
Copy link
Copy Markdown
Author

amarcin commented May 2, 2026

Added a second commit for a related defect surfaced by Codex CLI & Desktop 0.128:

Codex emits placeholder assistant messages with content: [] between tool-call turns. The existing Bedrock translator converts these to a Bedrock message whose Content slice is empty, which Bedrock Converse rejects:

Value null at 'messages.N.member.content' failed to satisfy constraint: Member must not be null

The new commit skips user/assistant turns whose converted content is empty, in both chat and responses paths. Full Codex session (developer role + empty assistant placeholders + tool use) now completes end-to-end against Bedrock Converse via Bifrost.

Verified live on a Bifrost v1.4.24 container patched with both commits.

coderabbitai[bot]
coderabbitai Bot previously approved these changes May 2, 2026
Streaming responses emitted via Bedrock were sending correct per-token
response.output_text.delta events but dropping the accumulated text on
the terminal events:

  - response.output_text.done     → text=""
  - response.content_part.done    → part.text=""
  - response.output_item.done     → item.content=[]
  - response.completed            → response.output=null

Clients that accumulate deltas themselves (OpenAI SDK, Codex CLI) were
unaffected. Clients that rehydrate from terminal events (Codex Desktop)
showed empty responses because the authoritative post-stream state said
the message had no content.

Track accumulated text in a new state.TextBuffers map (keyed by
output_index), populated from both text-delta emitters. The done/
completed emitters now retrieve and emit that text instead of a
placeholder empty string.

Reproduced live on Codex Desktop 0.128.0-alpha.1 → Bifrost → Bedrock
(Opus 4.7): empty bubbles before, full text after.

Part of maximhq#2492
@amarcin
Copy link
Copy Markdown
Author

amarcin commented May 2, 2026

Added a third commit for a related streaming bug surfaced while verifying Codex Desktop 0.128:

The Bedrock responses-stream translator emits per-token response.output_text.delta events correctly, but drops the accumulated text on every terminal event (currently hardcodes emptyText := ""):

  • response.output_text.donetext=""
  • response.content_part.donepart.text=""
  • response.output_item.doneitem.content=[]
  • response.completedresponse.output=null

Clients that build the message by accumulating deltas (OpenAI SDK, Codex CLI) were unaffected. Clients that rehydrate from the terminal events (Codex Desktop) rendered empty bubbles because the authoritative post-stream state said the message had no content.

The new commit adds a TextBuffers map[int]string to BedrockResponsesStreamState, populated from both text-delta emit sites, and plumbs the accumulated text into the four done/completed emitters. Reproduced fixed live on Codex Desktop.

@amarcin amarcin changed the title fix(bedrock): map role="developer" to system in chat and responses fix(bedrock): developer-role, empty-content, and streaming terminal events May 2, 2026
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.

[Bug]: Role developer is not accepted for bedrock or vertex providers

2 participants