anomalyco / anomalyco/opencode
Qwen3.8-27B + SGLang (RadixArk/MiaAI DGX Spark): System message must be at the beginning — working plugin coalesce
@neriousy is already working on this.
Since Aug 25, 2026.
- Dominant language
- TypeScript
- Stars
- 209k
- Forks
- 27.5k
- PR merge metrics
- PR metrics pending
Description
Summary
OpenCode 1.18.23 still emits multiple role: "system" fragments to OpenAI-compatible backends. Serving Qwen3.8-27B with SGLang on NVIDIA DGX Spark using the stock HuggingFace chat template (strict: only messages[0] may be system) makes every agent turn fail with:
Bad Request: System message must be at the beginning.
The same OpenCode config / same short model id worked under vLLM. After switching the inference stack to the MiaAI / RadixArk SGLang-on-DGX-Spark recipe, OpenCode broke — the model weights are Qwen3.8, the engine + raw HF template are what changed.
Related: #15059, #16560, #20785, #20813 · PRs #15018, #16981, #23656.
Serving stack (keywords)
Deployed from / based on:
- Repo: https://github.com/MiaAI-Lab/Qwen3.8-27B-SGLang-DGX-Spark
- Cookbook: https://docs.sglang.io/cookbook/autoregressive/Qwen/Qwen3.8-27B
- Weights (default QUANT=nvfp4):
RadixArk/Qwen3.8-27B-NVFP4-BF16-LMHead
(also:RadixArk/Qwen3.8-27B-NVFP4,Qwen/Qwen3.8-27B,Qwen/Qwen3.8-27B-FP8) - Docker image:
lmsysorg/sglang:qwen38-27b - Engine: SGLang (not vLLM) —
/returns"SGLang is running",/v1/models→owned_by: "sglang" - Hardware: NVIDIA DGX Spark / GB10 (aarch64)
- Spec modes in that repo: DSpark / EAGLE-MTP / DFlash2 (we hit the same template guard regardless)
- Reasoning / tools (server):
--reasoning-parser qwen3, tool parserqwen3_coder - Context: native 262144
- Default served name in MiaAI scripts:
SERVED_MODEL_NAME=qwen3.8-27b-sglang - Our OpenCode alias (intentionally shortened to match older config): API model id
qwen3.8-27b→ OpenCodedgx/qwen3.8-27b
Live identity probes also reported e.g. model_type: qwen3_5, architecture Qwen3_5ForConditionalGeneration, SGLang version string like 0.0.0.dev0+qwen38.27b.….
Repro
Server (template):
# OK — single system at [0]
curl …/v1/chat/completions -d '{
"model":"qwen3.8-27b",
"messages":[{"role":"system","content":"Be brief."},{"role":"user","content":"Say OK"}]
}'
# 400 — system not first
# "System message must be at the beginning."
# 400 — agent-like second system after tools
# [system, user, assistant+tools, tool, system, user]
OpenCode (before workaround):
opencode run -m dgx/qwen3.8-27b 'Reply with exactly: PONG'
# → Error: Bad Request: System message must be at the beginning.
Workaround that works (plugin, no OpenCode fork)
Last plugin in plugin[] coalesces via experimental hooks (in-place mutate; reassign is a no-op):
experimental.chat.system.transform— join alloutput.system[]with\n\n→ one fragmentexperimental.chat.messages.transform— merge allsystem/developermessages → onemessages[0]
Optional model config (documents intent; may be ignored until core lands):
"systemMessage": "single"
After plugin + restart:
opencode run -m dgx/qwen3.8-27b 'Reply with exactly: PONG'
# → PONG
Plugin sketch
export default async function QwenSingleSystem() {
const mergeSystemFragments = (system) => {
if (!Array.isArray(system) || system.length <= 1) return
const merged = system
.map((s) => (typeof s === "string" ? s : String(s ?? "")))
.map((s) => s.trim())
.filter(Boolean)
.join("\n\n")
system.splice(0, system.length, merged || "")
}
const collapseMessageSystems = (messages) => {
if (!Array.isArray(messages) || messages.length === 0) return
const systemIdx = []
for (let i = 0; i < messages.length; i++) {
const role = messages[i]?.info?.role
if (role === "system" || role === "developer") systemIdx.push(i)
}
if (systemIdx.length <= 1) {
if (systemIdx.length === 1 && systemIdx[0] !== 0) {
const [sys] = messages.splice(systemIdx[0], 1)
messages.unshift(sys)
}
return
}
const texts = []
for (const i of systemIdx) {
for (const p of messages[i]?.parts || []) {
if (p?.type === "text" && p.text) texts.push(String(p.text).trim())
}
}
const mergedText = texts.filter(Boolean).join("\n\n")
for (let k = systemIdx.length - 1; k >= 0; k--) messages.splice(systemIdx[k], 1)
messages.unshift({
info: { role: "system" },
parts: mergedText ? [{ type: "text", text: mergedText }] : [],
})
}
return {
"experimental.chat.system.transform": async (_input, output) => {
mergeSystemFragments(output.system)
},
"experimental.chat.messages.transform": async (_input, output) => {
collapseMessageSystems(output.messages)
},
}
}
Ask
Please land a core equivalent (systemMessage: "single" / merge before provider send for openai-compatible / non-Anthropic), so SGLang + raw HF Qwen3.8 (RadixArk NVFP4 on DGX Spark, lmsysorg/sglang:qwen38-27b) works without a local plugin.
Happy to test nightlies against this stack.
Note
We will drop the plugin when upstream merge ships and stays stable. If this coalesce causes context loss / instability, we will revert and report.
Contributor guide
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.
Assessment
This issue has not been assessed yet.