Visitar URL original
[fix]: strip role from non-message OpenAI Responses input items by nettee · Pull Request #5102 · maximhq/bifrost · GitHub
Skip to content

[fix]: strip role from non-message OpenAI Responses input items - #5102

Merged
akshaydeo merged 3 commits into
maximhq:devfrom
nettee:fix/openai-responses-non-message-role
Jul 12, 2026
Merged

akshaydeo merged 3 commits into
maximhq:devfrom
nettee:fix/openai-responses-non-message-role

Conversation

@nettee

@nettee nettee commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes Chat-to-Responses tool-call replay against the OpenAI Responses API.

Vercel AI SDK automatically replays an assistant tool call and its tool result on the second step of generateText. Bifrost converts the assistant tool call into a Responses function_call input item, but the item currently retains role: "assistant". OpenAI rejects that wire shape with:

invalid_request_error: Unknown parameter: input[1].role

This PR enforces the OpenAI wire invariant at the provider seam: modeled input items with an explicit type other than message do not carry role.

This PR is intentionally scoped to the role bug. It does not include reasoning-summary normalization.

Changes

  • Clear role from modeled OpenAI Responses input items whose explicit type is not message.
  • Preserve role on normal message items.
  • Preserve caller input immutability by normalizing the per-item value copy.
  • Add a focused wire regression test for a replayed function_call with an assistant role.
  • Add the required Core changelog entry.

Type of change

  • Bug fix
  • Feature
  • Refactor
  • Documentation
  • Chore/CI

Affected areas

  • Core (Go)
  • Transports (HTTP)
  • Providers/Integrations
  • Plugins
  • UI (React)
  • Docs

How to test

Go regression tests

cd core
go test ./providers/openai -count=1

The regression test verifies that:

  • a message item retains its role;
  • a function_call item omits role on the final wire request;
  • call ID, function name, and arguments remain unchanged;
  • caller-owned input roles are not mutated.

Real-provider black-box reproduction

The full sanitized reproduction is in #5101. It uses:

Vercel AI SDK
  -> local Bifrost /v1/chat/completions
  -> compat Chat-to-Responses conversion
  -> real OpenAI /v1/responses

The SDK receives only an initial prompt and one executable local tool. It automatically constructs request two with the assistant tool call and matching tool result.

Base dev@e0d2ac178:

{
  "requestCount": 2,
  "request1ReplayAbsent": true,
  "validSecondReplay": true,
  "secondStatus": 400,
  "errorType": "invalid_request_error",
  "errorParam": "input[1].role",
  "errorCode": "unknown_parameter"
}

With this commit, using the identical locked harness and request topology:

{
  "requestCount": 2,
  "request1ReplayAbsent": true,
  "validSecondReplay": true,
  "secondStatus": 200,
  "errorType": null,
  "errorParam": null,
  "errorCode": null
}

The same run also builds the current bifrost-http checkout before starting the local server.

golangci-lint was not available in the local environment, so the lint target was not run.

Screenshots/Recordings

Not applicable.

Breaking changes

  • Yes
  • No

The change only removes a field that OpenAI rejects on non-message Responses input items. Role-bearing message items are unchanged.

Related issues

Closes #5101

Possibly related context: #1977. This PR does not assume that issue is a duplicate.

Security considerations

  • No credentials or secret values are added.
  • The live reproduction supplied OPENAI_API_KEY only to the local Bifrost process.
  • No authentication, authorization, storage, or logging behavior changes.

Checklist

  • I read the contribution guidelines and followed the commit, changelog, and branch conventions.
  • I added a focused wire regression test.
  • I added the required Core changelog entry.
  • I verified the focused OpenAI provider package tests pass.
  • I verified the current Bifrost binary builds and the real-provider reproduction succeeds.
  • I reviewed the final diff for unrelated files and secrets.
  • git diff --check passes.
  • I ran golangci-lint locally (tool unavailable).

Affected packages:
- core/providers/openai/
- core/changelog.md

Closes maximhq#5101.
@CLAassistant

CLAassistant commented Jul 10, 2026 •

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@coderabbitai

coderabbitai Bot commented Jul 10, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 6711d489-98b1-4849-912e-973d46e051fb

📥 Commits

Reviewing files that changed from the base of the PR and between 9228d4a and 5c3cf4f.

📒 Files selected for processing (1)
  • core/changelog.md

📝 Walkthrough

Summary by CodeRabbit

  • Bug Fixes
    • Fixed OpenAI Responses request serialization to omit role for non-message input items (such as function calls), avoiding unsupported fields in the outgoing payload.
    • Confirmed that proper message input items still keep their expected role.
    • Ensured the original request data remains unchanged while preparing the payload.
  • Tests
    • Added a unit test validating the wire-format output for mixed message and function-call inputs, including correct omission of role.
  • Documentation
    • Updated the changelog to document the fix.

Walkthrough

OpenAI Responses request conversion now removes role from non-message input items while preserving it for message items. A wire-format test covers mixed message and function-call inputs, verifies related fields, and confirms the original request remains unchanged.

Changes

Responses role normalization

Layer / File(s) Summary
Role normalization and wire-format validation
core/providers/openai/responses.go, core/providers/openai/responses_test.go, core/changelog.md
ToOpenAIResponsesRequest clears roles for non-message items; tests verify serialized fields and input immutability, and the changelog records the fix.

Estimated code review effort: 2 (Simple) | ~10 minutes

Suggested reviewers: danpiths, akshaydeo, TejasGhatte, roroghost17

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main fix: stripping role from non-message Responses input items.
Description check ✅ Passed The PR description covers the summary, changes, testing, related issues, security, and checklist items required by the template.
Linked Issues check ✅ Passed The code and test changes enforce role omission for non-message Responses items and cover the reported replay failure in #5101.
Out of Scope Changes check ✅ Passed All changes are directly related to the role-stripping bug, regression test, and changelog entry; no unrelated edits are evident.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ 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.

@greptile-apps

greptile-apps Bot commented Jul 10, 2026 •

Copy link
Copy Markdown
Contributor

Confidence Score: 5/5

This looks safe to merge.

  • No blocking issues found in the changed code.

Important Files Changed

Filename Overview
core/providers/openai/responses.go Normalizes OpenAI Responses input items so non-message items omit role before request serialization.
core/providers/openai/responses_test.go Adds a regression test for message role preservation, function-call role omission, field preservation, and input immutability.
core/changelog.md Adds the Core changelog entry for the OpenAI Responses role normalization fix.

Reviews (3): Last reviewed commit: "Merge branch 'dev' into fix/openai-respo..." | Re-trigger Greptile

Comment thread core/providers/openai/responses.go

@coderabbitai coderabbitai 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.

🧹 Nitpick comments (1)
core/providers/openai/responses_test.go (1)

2085-2157: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cover function_call_output replay as well.

This regression test only exercises function_call, while the PR objective also requires function_call_output items to omit role. Add that item to the fixture and assert its wire representation so both tool-replay directions remain protected.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/providers/openai/responses_test.go` around lines 2085 - 2157, Extend
TestToOpenAIResponsesRequest_OmitsRoleFromNonMessageInputItems to include a
function_call_output input item with a role and representative output fields,
then assert its serialized wire item omits role while preserving the expected
fields, alongside the existing function_call assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@core/providers/openai/responses_test.go`:
- Around line 2085-2157: Extend
TestToOpenAIResponsesRequest_OmitsRoleFromNonMessageInputItems to include a
function_call_output input item with a role and representative output fields,
then assert its serialized wire item omits role while preserving the expected
fields, alongside the existing function_call assertions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: cd4c4b1a-6ff5-474e-9367-b7c5a706203d

📥 Commits

Reviewing files that changed from the base of the PR and between e0d2ac1 and 98bb911.

📒 Files selected for processing (3)
  • core/changelog.md
  • core/providers/openai/responses.go
  • core/providers/openai/responses_test.go

coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 10, 2026
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Signed-off-by: nettee <nettee.liu@gmail.com>
coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 11, 2026
akshaydeo
akshaydeo previously approved these changes Jul 12, 2026
Signed-off-by: Akshay Deo <akshay@akshaydeo.com>
@akshaydeo
akshaydeo dismissed stale reviews from coderabbitai[bot] and themself via 5c3cf4f July 12, 2026 14:44
@akshaydeo
akshaydeo merged commit 8147100 into maximhq:dev Jul 12, 2026
3 of 5 checks passed
@coderabbitai
coderabbitai Bot requested a review from akshaydeo July 12, 2026 14:45
akshaydeo added a commit that referenced this pull request Jul 14, 2026
* [fix]: OpenAI provider - omit role from non-message Responses items

Affected packages:
- core/providers/openai/
- core/changelog.md

Closes #5101.

* Update core/providers/openai/responses.go

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Signed-off-by: nettee <nettee.liu@gmail.com>

---------

Signed-off-by: nettee <nettee.liu@gmail.com>
Signed-off-by: Akshay Deo <akshay@akshaydeo.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: Akshay Deo <akshay@akshaydeo.com>
akhsaul pushed a commit to akhsaul/bifrost that referenced this pull request Aug 27, 2026
…mhq#5102)

* [fix]: OpenAI provider - omit role from non-message Responses items

Affected packages:
- core/providers/openai/
- core/changelog.md

Closes maximhq#5101.

* Update core/providers/openai/responses.go

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Signed-off-by: nettee <nettee.liu@gmail.com>

---------

Signed-off-by: nettee <nettee.liu@gmail.com>
Signed-off-by: Akshay Deo <akshay@akshaydeo.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: Akshay Deo <akshay@akshaydeo.com>
occcat pushed a commit to occcat/bifrost that referenced this pull request Sep 2, 2026
…mhq#5102)

* [fix]: OpenAI provider - omit role from non-message Responses items

Affected packages:
- core/providers/openai/
- core/changelog.md

Closes maximhq#5101.

* Update core/providers/openai/responses.go

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Signed-off-by: nettee <nettee.liu@gmail.com>

---------

Signed-off-by: nettee <nettee.liu@gmail.com>
Signed-off-by: Akshay Deo <akshay@akshaydeo.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: Akshay Deo <akshay@akshaydeo.com>
Pratham-Mishra04 pushed a commit that referenced this pull request Sep 7, 2026
## Summary

Adds two new harness folders (69 and 70) and fixes a vacuous-pass bug in folder 19 that allowed regression tests to silently prove nothing. Folder 69 pins the `finish_reason` derivation fix (#6831) on the compat chat→responses path. Folder 70 pins the correctness of `reasoning_summary_*` streaming event fields across Gemini, Vertex, Anthropic, and Bedrock.

## Changes

- **Folder 19 (chat-responses-tool-replay):** Rows 1–2 were running against `openai/gpt-4o-mini`, a chat-capable model that `markForConversion` never converts. The assertions passed vacuously because the replayed tool call never reached `ToResponsesRequest`. Both rows are now on `bedrock_mantle/openai.gpt-5.6-luna`, which is responses-only and always converts. A guard assertion (`extra_fields.converted_request_type == 'responses'`) was added to both rows so a future model swap cannot silently re-introduce the same vacuous-pass.

- **Folder 69 (finish-reason-derivation, #6831):** `ToBifrostChatResponse` never set `FinishReason`, and `BifrostResponseChoice.FinishReason` has no `omitempty`, so every non-streaming converted reply serialized `"finish_reason": null`, breaking agent loops that key on `tool_calls`. Four cases cover: `stop` (plain reply), `tool_calls` (forced via `tool_choice=required`), `length` (truncated by `max_completion_tokens`), and the streaming terminal chunk. Every row asserts `converted_request_type == 'responses'` first to prevent vacuous passes on chat-capable models.

- **Folder 70 (reasoning-summary-fields):** The `reasoning_summary_*` streaming events were missing required fields (`summary_index`, `text`, `part`) per the OpenAI Responses streaming spec, and the reasoning item they described was emitted empty. Ten cases cover: event field completeness on Gemini and Vertex; the reasoning item carrying its summary into `response.completed` on Gemini; `summary_index` on Anthropic; Bedrock's `reasoning_summary_text.done` carrying the accumulated delta text rather than a hardcoded empty string; Bedrock reasoning items carrying both `summary` and `encrypted_content` and replaying without a 400; Gemini 3 thought signatures surviving the streaming path; and the `/genai` egress reuniting a signature with its text on one part for both Gemini 3 and Anthropic.

- **Backlog doc:** Marked "Reasoning summary" as complete and added the folder 69 entry to the translation-layer section.

## Type of change

- [ ] Bug fix
- [ ] Feature
- [ ] Refactor
- [ ] Documentation
- [x] Chore/CI

## Affected areas

- [ ] Core (Go)
- [ ] Transports (HTTP)
- [x] Providers/Integrations
- [ ] Plugins
- [ ] UI (React)
- [ ] Docs

## How to test

Run the provider harness collection against a live environment with credentials for Gemini, Vertex, Anthropic, Bedrock (global Claude Sonnet 4.5), and `bedrock_mantle/openai.gpt-5.6-luna`. Execute folders 19, 69, and 70 in order (folder 70 cases 6–7 depend on a collection variable set by case 6).

```sh
# Run via Newman or the Postman UI against {{baseUrl}} pointed at a running Bifrost instance.
newman run tests/e2e/api/collections/provider-harness.json \
  --folder "19. Chat-to-Responses Tool Replay" \
  --folder "69. Responses -> Chat finish_reason derivation" \
  --folder "70. Reasoning Summary Streaming Event Fields" \
  --env-var "baseUrl=http://localhost:8080"
```

Expected outcomes:
- Folder 19 rows 1–2: `converted_request_type == 'responses'` passes; no `input[N].role` error.
- Folder 69: `finish_reason` is `stop`, `tool_calls`, or `length` as appropriate; streaming terminal chunk carries exactly one non-null `finish_reason`.
- Folder 70: all `reasoning_summary_*` events carry `summary_index`; `done` events carry non-empty `text`; `done` text matches accumulated deltas; reasoning items carry non-empty `summary`; Bedrock replay returns 2xx; Gemini 3 signatures survive on at least one route.

## Breaking changes

- [ ] Yes
- [x] No

## Related issues

Closes #6831. References #5101, #5102.

## Security considerations

None. Changes are limited to test harness assertions and collection variable handling.

## Checklist

- [x] I read `docs/contributing/README.md` and followed the guidelines
- [x] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [x] I verified builds succeed (Go and UI)
- [x] I verified the CI pipeline passes locally if applicable
fearlessfe pushed a commit to fearlessfe/bifrost that referenced this pull request Oct 8, 2026
## Summary

Adds two new harness folders (69 and 70) and fixes a vacuous-pass bug in folder 19 that allowed regression tests to silently prove nothing. Folder 69 pins the `finish_reason` derivation fix (maximhq#6831) on the compat chat→responses path. Folder 70 pins the correctness of `reasoning_summary_*` streaming event fields across Gemini, Vertex, Anthropic, and Bedrock.

## Changes

- **Folder 19 (chat-responses-tool-replay):** Rows 1–2 were running against `openai/gpt-4o-mini`, a chat-capable model that `markForConversion` never converts. The assertions passed vacuously because the replayed tool call never reached `ToResponsesRequest`. Both rows are now on `bedrock_mantle/openai.gpt-5.6-luna`, which is responses-only and always converts. A guard assertion (`extra_fields.converted_request_type == 'responses'`) was added to both rows so a future model swap cannot silently re-introduce the same vacuous-pass.

- **Folder 69 (finish-reason-derivation, maximhq#6831):** `ToBifrostChatResponse` never set `FinishReason`, and `BifrostResponseChoice.FinishReason` has no `omitempty`, so every non-streaming converted reply serialized `"finish_reason": null`, breaking agent loops that key on `tool_calls`. Four cases cover: `stop` (plain reply), `tool_calls` (forced via `tool_choice=required`), `length` (truncated by `max_completion_tokens`), and the streaming terminal chunk. Every row asserts `converted_request_type == 'responses'` first to prevent vacuous passes on chat-capable models.

- **Folder 70 (reasoning-summary-fields):** The `reasoning_summary_*` streaming events were missing required fields (`summary_index`, `text`, `part`) per the OpenAI Responses streaming spec, and the reasoning item they described was emitted empty. Ten cases cover: event field completeness on Gemini and Vertex; the reasoning item carrying its summary into `response.completed` on Gemini; `summary_index` on Anthropic; Bedrock's `reasoning_summary_text.done` carrying the accumulated delta text rather than a hardcoded empty string; Bedrock reasoning items carrying both `summary` and `encrypted_content` and replaying without a 400; Gemini 3 thought signatures surviving the streaming path; and the `/genai` egress reuniting a signature with its text on one part for both Gemini 3 and Anthropic.

- **Backlog doc:** Marked "Reasoning summary" as complete and added the folder 69 entry to the translation-layer section.

## Type of change

- [ ] Bug fix
- [ ] Feature
- [ ] Refactor
- [ ] Documentation
- [x] Chore/CI

## Affected areas

- [ ] Core (Go)
- [ ] Transports (HTTP)
- [x] Providers/Integrations
- [ ] Plugins
- [ ] UI (React)
- [ ] Docs

## How to test

Run the provider harness collection against a live environment with credentials for Gemini, Vertex, Anthropic, Bedrock (global Claude Sonnet 4.5), and `bedrock_mantle/openai.gpt-5.6-luna`. Execute folders 19, 69, and 70 in order (folder 70 cases 6–7 depend on a collection variable set by case 6).

```sh
# Run via Newman or the Postman UI against {{baseUrl}} pointed at a running Bifrost instance.
newman run tests/e2e/api/collections/provider-harness.json \
  --folder "19. Chat-to-Responses Tool Replay" \
  --folder "69. Responses -> Chat finish_reason derivation" \
  --folder "70. Reasoning Summary Streaming Event Fields" \
  --env-var "baseUrl=http://localhost:8080"
```

Expected outcomes:
- Folder 19 rows 1–2: `converted_request_type == 'responses'` passes; no `input[N].role` error.
- Folder 69: `finish_reason` is `stop`, `tool_calls`, or `length` as appropriate; streaming terminal chunk carries exactly one non-null `finish_reason`.
- Folder 70: all `reasoning_summary_*` events carry `summary_index`; `done` events carry non-empty `text`; `done` text matches accumulated deltas; reasoning items carry non-empty `summary`; Bedrock replay returns 2xx; Gemini 3 signatures survive on at least one route.

## Breaking changes

- [ ] Yes
- [x] No

## Related issues

Closes maximhq#6831. References maximhq#5101, maximhq#5102.

## Security considerations

None. Changes are limited to test harness assertions and collection variable handling.

## Checklist

- [x] I read `docs/contributing/README.md` and followed the guidelines
- [x] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [x] I verified builds succeed (Go and UI)
- [x] I verified the CI pipeline passes locally if applicable
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]: Chat-to-Responses tool replay sends role on function_call input items

3 participants