[Compatibility] Qwen3.8-27B via LiteLLM hosted_vllm: 400 "System message must be at the beginning." (openai-chat wire sends system+developer)
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 82/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Active
- Tech stack
- typescript
Research direction
The workaround lives in src/adapters/openai-chat/messages.ts as qwen38LeadingSystemTemplate. Widen the model-id regex so a LiteLLM-style prefix such as openai/Qwen3.8-27B still matches, then confirm the adapter emits a single leading system (or demoted developer) instead of [system, developer, user]. Done when that namespaced id no longer produces the vLLM 400.
Written by the indexing model from the issue text.
Description
Client or integration
Codex CLI
Provider or upstream service
LiteLLM -> hosted_vllm -> vLLM serving Qwen/Qwen3.8-27B (OpenAI-compatible endpoint)
OpenCodex version
2.78.0
Endpoint or capability
/v1/chat/completions (openai-chat adapter; Codex responses -> chat translation)
Current behaviour
When routing Codex to an OpenAI-compatible endpoint that serves Qwen3.8-27B through LiteLLM + vLLM, every request fails with HTTP 400.
litellm.BadRequestError: Hosted_vllmException - {"error":{"message":"System message must be at the beginning.","type":"BadRequestError","param":null,"code":400}}
The proxy (openai-chat adapter, messagesToChatFormat in src/adapters/openai-chat/messages.ts) emits a leading system message for the system prompt, then a separate developer message for Codex's developer/instructions turn:
[
{
"role": "system",
"content": "<system prompt>"
},
{
"role": "developer",
"content": "<developer instructions>"
},
{
"role": "user",
"content": "..."
}
]
Qwen3.8-27B's pinned vLLM chat template treats both system and developer as leading instruction blocks, so system followed by developer is rejected with "System message must be at the beginning."
Reproduced directly against the endpoint:
[system, user]-> 200 OK[system, developer, user]-> 400 "System message must be at the beginning."[developer, user](no leading system) -> 200 OK[system, user, user](developer demoted to user) -> 200 OK
Expected behaviour
Any served name for Qwen3.8-27B (e.g. openai/Qwen3.8-27B, a common LiteLLM provider prefix) should work through the proxy. The developer instruction should either be folded into the single leading system message or emitted with a role the model accepts (user), instead of producing a leading system + developer pair.
Codex itself expects the agent/developer instructions to be preserved; the point is the wire encoding, not dropping the content.
Minimal redacted request or reproduction
curl https://llm.xxx.com/v1/chat/completions \
-H "Authorization: Bearer <key>" -H "Content-Type: application/json" \
-d '{"model":"openai/Qwen3.8-27B","messages":[
{"role":"system","content":"You are a helpful assistant."},
{"role":"developer","content":"developer instruction"},
{"role":"user","content":"hi"}],
"max_tokens":10}'
The same endpoint accepts `[system, user]` and `[developer, user]`.
opencodex provider config `Varmmego-Qwen3.8-27B`:
- adapter: openai-chat
- defaultModel: openai/Qwen3.8-27B
- foldDeveloperRoleToSystem: false
Actual response or error
HTTP 400
{"error":{"message":"litellm.BadRequestError: Hosted_vllmException - {\"error\":{\"message\":\"System message must be at the beginning.\",\"type\":\"BadRequestError\",\"param\":null,\"code\":400}}. Received Model Group=openai/Qwen3.8-27B\nAvailable Model Group Fallbacks=None","type":null,"param":null,"code":"400"}}
Upstream documentation
Qwen3.8-27B model: https://huggingface.co/Qwen/Qwen3.8-27B
The chat template is model-owned (pinned in the served vLLM tokenizer); the error originates from vLLM request pre-validation, not from LiteLLM. LiteLLM is only a thin OpenAI-compatible proxy in front of vLLM.
Suggested mapping or implementation notes
Incomplete workaround is the root cause
opencodex 2.78 ALREADY ships a model-specific workaround for this exact model. In src/adapters/openai-chat/messages.ts, qwen38LeadingSystemTemplate demotes a developer message to user when it is enabled, precisely because Qwen3.8-27B's pinned vLLM chat template rejects a non-leading system and every developer role.
However, the trigger regex only matches Qwen/Qwen3.8-27B or the bare Qwen3.8-27B:
const qwen38LeadingSystemTemplate = !nativeOpenAI && /^(?:Qwen\/)?(?:Qwen3\.8-27B|qwen3-8-27b(?:-(?:fp8|lora))?)$/i.test(parsed.modelId);
It does NOT match a namespaced id like openai/Qwen3.8-27B, which is the form LiteLLM exposes (the openai/ prefix comes from the LiteLLM provider config, not from Qwen). So for users routing Qwen3.8-27B through LiteLLM (a very common setup), the workaround never fires and the request still goes out as [system, developer, user] -> 400.
Net effect: the workaround exists for this model family but is ineffective for the most common deployment shape.
Suggested fix
Accept an optional provider prefix before the model name, e.g. /(?:^|\/)Qwen3\.8-27B$/i, or key the behaviour off the underlying served model rather than the full routed id.
Additional context and attachments
Reported from a Codex -> opencodex 2.78.0 -> LiteLLM -> vLLM setup on Windows.
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.
- Dominant language
- TypeScript
- Stars
- 16.9k
- Forks
- 1.3k
- Avg merge
- 11h 47m
- Merged PRs (30d)
- 649
Getting set up
This project ships no dev container, Dockerfile or contributing guide, so setting up is up to you: start from its README, and see our first-contribution guide for the general steps.
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from lidge-jun/opencodex
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
lidge-jun/opencodex#7812 · 1 comment · 1 reaction ·
Maintainers usually reply within 1 day
-
[Bug]: Windows client path overrides accept drive-less rooted pathsPossibly taken A pull request linked to this issue is open or already merged. Openbug platform
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
Maintainers usually reply within 1 day
-
[Bug]: MiniMax stale-roster refresh has no destination guard for Custom base URLsPossibly taken A pull request linked to this issue is open or already merged. Openbug catalog provider
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
lidge-jun/opencodex#7807 · 1 comment · 1 reaction ·
Maintainers usually reply within 1 day
-
[Feature]: Respect Google Retry-After as a lower bound for retriesPossibly taken A pull request linked to this issue is open or already merged. Openenhancement provider tools
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
Maintainers usually reply within 1 day
-
enhancement
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
lidge-jun/opencodex#7728 · 1 comment ·
Maintainers usually reply within 1 day
All issues in lidge-jun/opencodex
Similar issues
-
calcite-ui-icons enhancement impact - p3 - not time sensitive needs triage
Difficulty 2/5 1-3 hours Newbie friendliness 67/100
Esri/calcite-design-system#15467 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
diegosouzapw/OmniRoute#18390 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 65/100
-
Difficulty 1/5 1-3 hours Newbie friendliness 86/100
guilhermerodz/input-otp#157 · 1 comment ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
davidjerleke/embla-carousel#1414 ·
Maintainers usually reply within 4 days