Repository navigation
developer messages are folded into the system prompt on every Chat host except api.openai.com #5213
Description
Client or integration
OpenAI-compatible SDK
Provider or upstream service
Any openai-chat provider whose base URL host is not api.openai.com, for example openrouter
OpenCodex version
2.59.0 / commit 134c92a01b120162f00c7275189cc47858720379
Endpoint or capability
/v1/chat/completions, message role translation
Current behaviour
A developer role message reaches the upstream as developer only when the provider base URL host is exactly api.openai.com. On every other OpenAI-compatible Chat endpoint the message text is appended to the system prompt and the message itself is not emitted, so the upstream never sees the developer role and the instruction loses its position in the conversation.
The host test is at src/adapters/openai-chat/wire.ts:44:
export function isNativeOpenAIChatTarget(provider: OcxProviderConfig): boolean {
try {
return new URL(provider.baseUrl).hostname === "api.openai.com";
} catch {
return false;
}
}src/adapters/openai-chat/messages.ts:123 and :131 hoist the text:
const nativeOpenAI = isNativeOpenAIChatTarget(provider);
...
const developerSystemParts = nativeOpenAI || chronologicalSystem
? []
: context.messages
.map(developerSystemText)
.filter((part): part is string => part !== undefined && part.length > 0);
const systemParts = [
...(context.systemPrompt ?? []),
...developerSystemParts,
...(toolCatalogNudge ? [toolCatalogNudge] : []),
];src/adapters/openai-chat/messages.ts:156 then skips the original message, and the branch that
does emit rewrites the role:
if (msg.role === "developer" && !hasImages) {
if (!nativeOpenAI && !chronologicalSystem) break;
...
chatMsg = { role: nativeOpenAI ? "developer" : "system", content: text };
}There is one narrow exception, chronologicalSystem, which applies only when the model is deepseek-v4.1-flash and the registry destination is opencode-go.
Two things are lost for every other host. The role itself, which the Chat Completions specification defines and which carries a different precedence than system. And the position, since a mid-conversation instruction is moved out of the timeline into the prompt preamble.
Expected behaviour
A developer message should be forwarded as role: "developer" in its original position on OpenAI-compatible Chat endpoints, since developer is part of the Chat Completions message role set rather than an OpenAI-host-specific extension.
If some upstreams genuinely reject the role, the fallback is reasonable, but it should be keyed to a known incompatibility for that destination rather than to the negation of a single hostname. As written, every OpenAI-compatible gateway is assumed not to support a standard role until proven otherwise, including gateways that proxy OpenAI itself.
Minimal redacted request or reproduction
# Configure an openai-chat provider whose baseUrl host is not api.openai.com.
curl http://127.0.0.1:10100/v1/chat/completions \
-H 'content-type: application/json' \
-H 'authorization: Bearer REDACTED' \
-d '{
"model": "REDACTED_PROVIDER/REDACTED_MODEL",
"messages": [
{"role": "user", "content": "First turn."},
{"role": "developer", "content": "Answer in exactly one sentence."},
{"role": "user", "content": "Second turn."}
]
}'Actual response or error
"messages": [
{"role": "system", "content": "<existing system prompt>Answer in exactly one sentence."},
{"role": "user", "content": "First turn."},
{"role": "user", "content": "Second turn."}
]
The `developer` message is gone as a message. Its text is now part of the system block, ahead of both user turns rather than between them.Upstream documentation
https://developers.openai.com/api/reference/resources/chat.md
Suggested mapping or implementation notes
The narrow fix is to invert the default. Forward developer as developer and keep the fold as an opt-out for destinations known to reject the role, in the same shape as the existing chronologicalSystem exception or as a provider config flag alongside the other per-provider compatibility switches in src/types/provider.ts.
If inverting the default is considered too broad a behaviour change, a smaller step that still addresses the more damaging half is to keep the role rewrite but preserve position, emitting {role: "system", content: text} in the message's original slot instead of hoisting the text into the preamble. That leaves role precedence unchanged but stops instructions from moving across the conversation.
Additional context and attachments
- fix(claude): keep mid-conversation system messages in the timeline #4161 established that folding in-conversation instructions into the prompt preamble is harmful, and fixed it on the Claude inbound by minting chronological
role: "developer"items atsrc/claude/inbound.ts:359specifically to preserve timeline order. Those items then reach this adapter, which folds them again whenever the host is notapi.openai.com. So for a Claude Code session routed to a non-OpenAI Chat provider, the two behaviours work against each other. - [Bug]: Mid-conversation role: system messages in Claude inbound are aggregated into top-level instructions, destroying prompt cache prefix #4148 covers the same class of problem on the inbound side.
Checks
- I searched existing provider and compatibility issues.
- The request and response were redacted.
- The expected behaviour is based on an upstream specification or a concrete client requirement.
Activity
Yum-wu commentedon Sep 20, 2026
I would be happy to work on this!
The issue clearly points out that folding
developermessages into the prompt preamble moves them out of the chronological timeline, which breaks conversation precedence for non-api.openai.comhosts (and conflicts with the intent established in #4161).I plan to address this with the following approach:
developermessages intodeveloperSystemParts.api.openai.comendpoints, emit{ role: "system", content: text }in its original chronological position (mirroring thechronologicalSystempattern) rather than prepending it into the system prompt preamble.api.openai.comemitting{ role: "developer", content: text }.tests/adapters/openai/verifying message ordering and role mapping across native and non-native Chat targets.Will open a PR shortly.
fix(openai-chat): preserve chronological position of developer messag…
Ingwannu commentedon Sep 20, 2026
Confirmed: the current generic
openai-chatpath moves non-nativedevelopercontent out of its original turn position, so this is a real ordering bug rather than a display-only issue.#5237 is the active fix attempt. Its direction—preserve the instruction at a legal chronological boundary while retaining native
developeron OpenAI—is appropriate, but the current draft head does not parse and its tool-call test would place a synthetic system message between an assistant tool call and the matching tool result. I have requested changes there. We will keep this issue open until the replacement head preserves both ordinary chronology and tool-call adjacency and passes exact-head CI.fix: preserve caller constraints on the request path (#5211, #5210, #…
lidge-jun commentedon Sep 20, 2026
Fixed on
devby #5271 (41b66be6d880625055adb319aef9bce75300a42b).The four losses on this path were fixed together because they share one failure mode: the request arrives carrying an explicit constraint, the proxy drops it, and the response is a normal success. A test that asserts a successful call or a 200 sees none of them. The governing rule the branch applies is to preserve on a supported path, and to refuse or apply the operator's conversion policy on an unsupported one, rather than dropping silently and reporting success.
A
developermessage keeps its chronological position, and the role conversion is handled as a separate contract rather than as a side effect of placement. #5237 by @Yum-wu is carried with attribution for the position half.Closing on
dev. PRs here targetdev, so GitHub does not auto-close.fix(openai-chat): fold the developer role unless a destination record…