Skip to content

fix(responses): map mid-conversation system turns to developer role (#6954) - #7056

Merged
diegosouzapw merged 4 commits into
cheaper-inference:release/v3.8.49from
xier2012:fix/responses-developer-role-midconv-system-6954
Jul 18, 2026
Merged

diegosouzapw merged 4 commits into
cheaper-inference:release/v3.8.49from
xier2012:fix/responses-developer-role-midconv-system-6954

Conversation

@xier2012

Copy link
Copy Markdown
Contributor

Problem (#6954)

When translating an Anthropic/OpenAI Messages-format request to the OpenAI Responses API (openaiToOpenAIResponsesRequest in open-sse/translator/request/openai-responses/toResponses.ts), the first role: "system" (or role: "developer") message is extracted into instructions, but every subsequent mid-conversation system turn is silently dropped (continue).

Clients such as Claude Code v2.1.207 inject harness reminders as mid-stream role: "system" turns. Dropping them loses harness instructions entirely; mapping them to assistant (as older revisions did) misattributes harness-authored content as model output. The correct target is the Responses API developer role, which exists for exactly this purpose.

Root cause

In toResponses.ts, the system/developer branch only handled the first message:

if (role === "system" || role === "developer") {
  if (!hasSystemMessage) {
    result.instructions = typeof msg.content === "string" ? msg.content : "";
    hasSystemMessage = true;
  }
  continue; // <- subsequent system/developer turns dropped
}

Fix

After the first system turn (which still becomes instructions), subsequent system/developer turns are now emitted as developer-role message input items, preserving their content:

input.push({
  type: "message",
  role: "developer",
  content: buildResponsesTextParts(msg.content),
});

A small buildResponsesTextParts helper converts a string or text-part array into the Responses input_text part shape. The downstream Codex executor's convertSystemToDeveloperRole already converts any residual system items in-place, so emitting developer directly is compatible.

Test

Added mid-conversation system turns are preserved as developer-role input items (#6954) to tests/unit/openai-responses-request-split.test.ts:

  • A request with [system, user, system, assistant, system] history.
  • Asserts the first system becomes instructions.
  • Asserts exactly two developer-role input items are produced with the correct text.
  • Asserts no harness content leaks into assistant prose.

The case passes on the patched code and would fail (zero developer items / dropped content) on the unpatched main.

Fixes #6954.

@xier2012
xier2012 requested a review from diegosouzapw as a code owner July 13, 2026 16:21

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request ensures that mid-conversation system and developer turns are preserved as developer-role input items in the Responses API, rather than being dropped or misattributed. It also adds a unit test to verify this behavior. The review feedback suggests enhancing the robustness of the new buildResponsesTextParts helper function by handling raw string elements within content arrays to prevent potential data loss.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +58 to +67
if (Array.isArray(content)) {
const parts: unknown[] = [];
for (const partValue of content) {
const part = toRecord(partValue);
if (part.type === "text" || typeof part.text === "string") {
parts.push({ type: "input_text", text: toString(part.text) });
}
}
return parts.length > 0 ? parts : [{ type: "input_text", text: "" }];
}

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.

medium

If content is an array containing raw strings (e.g., ["instruction part"]), toRecord(partValue) will return {} and the string will be silently ignored. Handling raw string elements in the array makes the helper more robust and prevents potential bugs where system instructions are lost.

  if (Array.isArray(content)) {
    const parts: unknown[] = [];
    for (const partValue of content) {
      if (typeof partValue === "string") {
        parts.push({ type: "input_text", text: partValue });
      } else {
        const part = toRecord(partValue);
        if (part.type === "text" || typeof part.text === "string") {
          parts.push({ type: "input_text", text: toString(part.text) });
        }
      }
    }
    return parts.length > 0 ? parts : [{ type: "input_text", text: "" }];
  }

@diegosouzapw

Copy link
Copy Markdown
Collaborator

Thanks for the precise root-cause analysis and the clean fix. I ran the full openai-responses translator regression suite against this branch (72 tests across the surrounding files) plus your 2 new tests — everything passes with no regressions, and ESLint is clean.

One thing worth flagging so you're not surprised: this is one of two bugs feeding the same #6954 report. There's a second, upstream bug in claude-to-openai.ts (the Claude→OpenAI Chat Completions leg) where mid-conversation role: "system" turns were being collapsed to assistant before they ever reach the code you fixed here — see #6982 for that half. Both are needed together for the full pipeline fix (Claude-format request → toResponses.ts only sees the correct system role once #6982's fix is also in), and we're merging both. Yours is genuinely a standalone, correct fix for its layer either way — will land as-is after the base retarget to the active release branch. Thanks for the contribution.

@diegosouzapw
diegosouzapw changed the base branch from main to release/v3.8.49 July 15, 2026 08:18
@diegosouzapw
diegosouzapw merged commit ff89a3d into cheaper-inference:release/v3.8.49 Jul 18, 2026
3 checks passed
@diegosouzapw

Copy link
Copy Markdown
Collaborator

Thanks @xier2012! Merged into release/v3.8.49 — validated in the combined merge-train (all gates green). Clean, surgical series; much appreciated.

@diegosouzapw diegosouzapw mentioned this pull request Jul 23, 2026
HouMinXi pushed a commit to HouMinXi/OmniRoute that referenced this pull request Aug 2, 2026
…heaper-inference#6954) (cheaper-inference#7056)

* fix(responses): map mid-conversation system turns to developer role (cheaper-inference#6954)

* test(responses): add cheaper-inference#6954 mid-conversation system -> developer regression

* fix(6954): keep bare-string content parts in buildResponsesTextParts

* test(6954): cover array-form system content with bare string
diegosouzapw pushed a commit that referenced this pull request Sep 1, 2026
…12207)

The leading system message was read as `typeof content === "string" ?
content : ""`, so a Chat-Completions content-part array — valid for
`system`, and what every prompt-caching client sends — collapsed the
whole system prompt into an empty `instructions`. Upstream accepted the
request and reported a normal prompt_tokens count, so the model answered
with no instructions at all and nothing in the response said so.

Mid-conversation system turns already handled the array shape (#7056);
only the first one did not. Reuses buildResponsesTextParts() and joins
the text parts, since `instructions` is a string rather than a part array.

Co-authored-by: Vadim Zhyvylo <zhyvylo@involve.software>
muhamadgalihsaputra pushed a commit to niyatna/NiyatnaRoute that referenced this pull request Sep 27, 2026
…heaper-inference#6954) (cheaper-inference#7056)

* fix(responses): map mid-conversation system turns to developer role (cheaper-inference#6954)

* test(responses): add cheaper-inference#6954 mid-conversation system -> developer regression

* fix(6954): keep bare-string content parts in buildResponsesTextParts

* test(6954): cover array-form system content with bare string
muhamadgalihsaputra pushed a commit to niyatna/NiyatnaRoute that referenced this pull request Sep 27, 2026
…heaper-inference#12207)

The leading system message was read as `typeof content === "string" ?
content : ""`, so a Chat-Completions content-part array — valid for
`system`, and what every prompt-caching client sends — collapsed the
whole system prompt into an empty `instructions`. Upstream accepted the
request and reported a normal prompt_tokens count, so the model answered
with no instructions at all and nothing in the response said so.

Mid-conversation system turns already handled the array shape (cheaper-inference#7056);
only the first one did not. Reuses buildResponsesTextParts() and joins
the text parts, since `instructions` is a string rather than a part array.

Co-authored-by: Vadim Zhyvylo <zhyvylo@involve.software>
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.

fix(backend): Anthropic→Responses translation: mid-conversation system turns are misattributed as assistant messages instead of developer role

2 participants