interactions: Responses `input` items with role `system`/`developer` are sent as user turns, upstream system prompt left empty (Devin)
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 (ConvertOpenAIResponsesRequestToInteractions → parseInteractionsPayload) 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: onlyroot.Get("instructions")feedssystem_instructionresponsesInputItemToInteractions,case "message": role is only checked forassistant/model; everything else becomesuser_input- no
type == "" && role != ""→messagenormalization, so shorthand items fall to thedefaultbranch and becomeuser_inputregardless of role (a shorthandassistantitem 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 ===— nosystem_instruction=== DEVIN UPSTREAM REQUEST ===— nosystem_promptkey (it isomitemptyand 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/developeritems ininput(typed or shorthand) are treated as the system prompt and reach Devin's system prompt field, merged with top-levelinstructionswhen both are present — consistent with the Gemini Responses translator after #2791 and with themessagesfallback in the Devin executor. - Shorthand items without
typeare treated as messages, sorole: "assistant"maps tomodel_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