Skip to content

fix(rtk): treat a Responses input item with no type as a message - #3652

Open
ntdatt812 wants to merge 1 commit into
decolua:masterfrom
ntdatt812:fix/untyped-responses-message-item
Open

ntdatt812 wants to merge 1 commit into
decolua:masterfrom
ntdatt812:fix/untyped-responses-message-item

Conversation

@ntdatt812

Copy link
Copy Markdown
Contributor

injectResponsesInputSystem and containsPromptInResponsesInput both require item.type === "message" to recognise a message item in a Responses input[]. That field is optional — it defaults to "message" — and native clients leave it off. A system item written that way is invisible to the injector, so it falls through to the else branch and prepends a second system message beside the one already there.

Measured on master (90b52e06), running the injector directly:

const body = { input: [
  { role: "system", content: [{ type: "input_text", text: "you are a helper" }] },
  { role: "user",   content: [{ type: "input_text", text: "hi" }] },
]};
injectSystemPrompt(body, FORMATS.OPENAI_RESPONSES, "speak like a caveman");
items: 3
[{"type":"message","role":"system","content":[{"type":"input_text","text":"speak like a caveman"}]},
 {"role":"system","content":[{"type":"input_text","text":"you are a helper"}]},
 {"role":"user","content":[{"type":"input_text","text":"hi"}]}]

Two role:"system" items go upstream, and the caller's own system prompt is never appended to. This only reaches native-passthrough traffic: bodies this repo builds itself always stamp the field (translator/request/openai-responses.js:369), which is why nothing caught it.

Why "no type means message" is this repo's existing reading

Three other call sites already treat an absent type that way:

file line test
open-sse/executors/codex.js 53 item.role === "system" && (!item.type || item.type === "message")
open-sse/executors/grok-cli.js 79 item.role === "user" && (!type || type === "message")
open-sse/rtk/headroom.js 96 typeof item.type === "string" && item.type !== "message" — an item with no type string is not excluded

and tests/unit/continuity-strip.test.js:21 builds its own fixture without the field. systemInject.js was the outlier, so the fix follows the existing idiom instead of inventing a new one.

On the second half of the diff

I fixed the scan in containsPromptInResponsesInput alongside it, and it is narrower than it looks. appendToResponsesMessage already dedups (c.some(b => b && b.text === prompt), and dedupStringAppend for the string case), so the scan only decides the case where the prompt sits in a different system item than the one findIndex returns. The fifth test covers exactly that case, and it is the only one that fails if that half is reverted. Saying so explicitly in case you would rather drop it — the first half stands on its own.

Tests

Six tests in tests/unit/system-inject-untyped-item.test.js, including the two negatives that keep the change honest: a function_call item carrying a stray role field is still skipped, and a body with no system item at all still gets one prepended.

Mutation-checked in both directions:

reverted result
findIndex half 2 failed
containsPromptInResponsesInput half 1 failed
neither 6 passed

Suite

npx vitest run tests/unit --config tests/vitest.config.js

tests failed
master (90b52e06) 1673 75
this branch 1679 75

Same 75 by name, compared as a set rather than a count — no new failures, and the six added tests account for the delta. Those 75 are pre-existing on a clean master checkout on this host (Windows, Node 24); I have not touched them.

injectResponsesInputSystem and containsPromptInResponsesInput both required
item.type === "message" to recognise a message item. The Responses API lets an
input[] message item leave `type` off — it defaults to "message" — and native
clients do. Such a system item was therefore invisible to the injector, which
fell through to its else branch and prepended a second system message beside
the one already there:

  input: [{ role: "system", content: [{type:"input_text", text:"you are a helper"}] },
          { role: "user",   content: [{type:"input_text", text:"hi"}] }]

  ->  3 items, two of them role:"system", the original never appended to

The repo already reads an absent type as a message in three other places:

  open-sse/executors/codex.js:53     item.role === "system" && (!item.type || item.type === "message")
  open-sse/executors/grok-cli.js:79  item.role === "user"   && (!type || type === "message")
  open-sse/rtk/headroom.js:96        typeof item.type === "string" && item.type !== "message"

and tests/unit/continuity-strip.test.js builds its own fixture without the
field. systemInject.js was the outlier, so the fix follows the existing idiom
rather than introducing a new one.

The scan in containsPromptInResponsesInput is fixed alongside it. That half is
narrower than it looks: appendToResponsesMessage already dedups, so the scan
only decides the case where the prompt sits in a different system item than the
one findIndex returns. A test covers exactly that case.
@ntdatt812
ntdatt812 force-pushed the fix/untyped-responses-message-item branch from bfd079a to cc19c1a Compare September 9, 2026 03:31
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.

1 participant