Skip to content

developer messages are folded into the system prompt on every Chat host except api.openai.com #5213

Closed

Description

@Android-Login-Analysis

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

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

  1. Yum-wu commented on Sep 20, 2026

    @Yum-wu
    Contributor

    I would be happy to work on this!

    The issue clearly points out that folding developer messages into the prompt preamble moves them out of the chronological timeline, which breaks conversation precedence for non-api.openai.com hosts (and conflicts with the intent established in #4161).

    I plan to address this with the following approach:

    1. Preserve chronological ordering by default on the Chat Completions wire instead of unconditionally hoisting developer messages into developerSystemParts.
    2. For non-api.openai.com endpoints, emit { role: "system", content: text } in its original chronological position (mirroring the chronologicalSystem pattern) rather than prepending it into the system prompt preamble.
    3. Keep api.openai.com emitting { role: "developer", content: text }.
    4. Add focused regression tests in tests/adapters/openai/ verifying message ordering and role mapping across native and non-native Chat targets.

    Will open a PR shortly.

  2. Ingwannu commented on Sep 20, 2026

    @Ingwannu
    Owner

    Confirmed: the current generic openai-chat path moves non-native developer content 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 developer on 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.

  3. lidge-jun commented on Sep 20, 2026

    @lidge-jun
    Owner

    Fixed on dev by #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 developer message 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 target dev, so GitHub does not auto-close.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    providerProvider adapters, OpenAI-compat presets, upstream API quirksprovider-compatibilityProvider compatibility reports

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions