interactions: Responses `input` items with role `system`/`developer` are sent as user turns, upstream system prompt left empty (Devin)

Author: HakunmCreated Sep 16, 2026Updated Sep 17, 2026

Version: dev @ 8c6d4dc; same behavior on v7.3.4 Path: /v1/responses → interactions → Devin OAuth (devin/swe-2) Client: pi coding agent (Responses API); also reproducible with curl OS: CPA in Docker on Ubuntu/arm64

Summary

The OpenAI Responses API accepts the system prompt in two places: top-level instructions, or input items with role: "system" / "developer" (with or without an explicit "type": "message"). The interactions request translator only handles instructions. Any system/developer item inside input is converted to a user_input step, so:

  • the interactions payload has no system_instruction,
  • the Devin wire request has no system prompt (field 2 is omitted),
  • the prompt text is delivered as the first user message, indistinguishable from the user's own turns.

The sibling Responses translators do this correctly: the Gemini one normalizes a missing type to message and routes system/developer into systemInstruction (that was #2791), and the Devin executor's fallback for raw OpenAI messages also recognizes system/developer. Only the interactions path is missing both steps.

Evidence

Offline run of the real code path (ConvertOpenAIResponsesRequestToInteractionsparseInteractionsPayload) on dev @ 8c6d4dc, system prompt S, user turn hi:

client shape system_instruction Devin wire system prompt Devin prompts
top-level instructions: S S S [user: hi]
input: [{type:"message", role:"system", content:S}, …] absent empty [user: S, user: hi]
input: [{role:"system", content:S}, …] (shorthand) absent empty [user: S, user: hi]
input: [{role:"developer", content:S}, …] absent empty [user: S, user: hi]

Captured production request from a real client (User-Agent: pi (linux …)), structure only:

instructions: ABSENT
input: 388 items
  item[0]  type=<none> role=system  content_chars=53450
  type=<none> role=user x4, type=message role=assistant x54,
  type=function_call x162, type=function_call_output x162, type=reasoning x5
tools: 75

That 53 KB agent prompt reaches Devin as the first of ~200 user-role prompts rather than as the system prompt. (The table above is from running the translator and parseInteractionsPayload directly on that shape; SystemPrompt is json:"system_prompt,omitempty", so an empty value drops the key from the upstream request entirely.)

Cause — internal/translator/openai/interactions/responses/interactions_openai_responses_request.go

  • ConvertOpenAIResponsesRequestToInteractions: only root.Get("instructions") feeds system_instruction
  • responsesInputItemToInteractions, case "message": role is only checked for assistant/model; everything else becomes user_input
  • no type == "" && role != ""message normalization, so shorthand items fall to the default branch and become user_input regardless of role (a shorthand assistant item is also mis-typed as user)

Behavior note: because the text is still delivered (as a user turn), short instructions are usually still followed; in controlled tests a one-line instruction and a 53 KB prompt with embedded markers were honored either way. The defect is that the system/user distinction is lost on the wire, which matters for long agent prompts buried in long histories and for anything upstream that treats the system prompt differently from user turns.

Related: SanitizeDevinSystemPrompt drops lines by generic substring

internal/runtime/executor/helps/devin_wire.go SanitizeDevinSystemPrompt removes whole lines that contain authorized security testing or destructive techniques, DoS attacks (alongside the Claude-Code-specific filters such as You are Claude Code, Claude Code is available as a CLI). Those two patterns are generic wording, so a non-Claude-Code harness whose policy text uses the same phrases silently loses the line. Example (no sensitive-words configured):

IN  (5 lines)                                             OUT (3 lines)
You are DeepSeek Harness.                                 You are DeepSeek Harness.
Assist with defensive security tasks only; … authorized   (dropped)
  security testing …
Always read a file before editing it.                     Always read a file before editing it.
Claude Code is available as a CLI tool.                   (dropped — expected, Claude-specific)
Finish by running the test suite.                         Finish by running the test suite.

The client has no way to know a line was removed.

Steps to reproduce

curl https://<cpa>/v1/responses -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' -d '{
  "model":"devin/swe-2","stream":false,
  "input":[{"role":"system","content":"MARKER-123: you are a terse assistant."},
           {"role":"user","content":"Reply with exactly: OK"}]}'

With request-log: true, open the resulting logs/v1-responses-*.log and compare two sections:

  • === INTERMEDIATE INTERACTIONS === — no system_instruction
  • === DEVIN UPSTREAM REQUEST === — no system_prompt key (it is omitempty and the value is empty); prompts[0] carries the marker text with "role": "user"

Sending the same text as top-level instructions instead produces system_instruction and system_prompt as expected, with prompts holding only the real user turn.

Expected behavior

  • Leading system/developer items in input (typed or shorthand) are treated as the system prompt and reach Devin's system prompt field, merged with top-level instructions when both are present — consistent with the Gemini Responses translator after #2791 and with the messages fallback in the Devin executor.
  • Shorthand items without type are treated as messages, so role: "assistant" maps to model_output.
  • The system prompt sanitizer only applies its Claude-Code-specific line filters to prompts that are identifiably Claude Code's; other clients' policy lines are forwarded unchanged.

Source: router-for-me/CLIProxyAPI