Skip to content

fix: merge injected system block into the first system message - #1

Closed
nicksosnovsky wants to merge 1 commit into
sehaxe:masterfrom
nicksosnovsky:fix/system-message-order
Closed

nicksosnovsky wants to merge 1 commit into
sehaxe:masterfrom
nicksosnovsky:fix/system-message-order

Conversation

@nicksosnovsky

Copy link
Copy Markdown

Problem

Every request fails with litellm.BadRequestError: OpenAIException - System message must be at the beginning (HTTP 400) when using OpenAI-compatible proxies backed by litellm/vLLM (e.g. qwen via a litellm proxy).

Root cause

The experimental.chat.system.transform hook appended the memory/goal/rules block as a second system message:

if (block) output.system.push(block)

opencode sends every element of output.system as a separate {role: "system"} message. Some servers reject more than one system message even when they are all at the beginning.

Repro

curl https://api-llm.novacom.by/v1/chat/completions -H 'Content-Type: application/json' -d '{
  "model": "qwen3.6-27b",
  "messages": [
    {"role": "system", "content": "You are a helpful assistant."},
    {"role": "system", "content": "Extra block."},
    {"role": "user", "content": "hi"}
  ]
}'

-> 400 System message must be at the beginning. Removing the second system message fixes it.

Fix

Append the block to the existing first system message instead of pushing a new one:

if (output.system.length > 0) output.system[0] += "\n\n" + block
else output.system.push(block)

Verification

  • Direct API call with the merged system message succeeds.
  • bun test bulba.test.ts: 45 pass / 1 fail (the failing search_memory test also fails on clean master - Windows environment, unrelated to this change).
  • The test suite calls the hook with an empty system: [] array, which still works via the push fallback.

The plugin appended a second system message via experimental.chat.system.transform,
which proxies like litellm/vLLM reject with 'System message must be at the beginning'
(400). Append the block to the existing system message instead.
@sehaxe sehaxe self-assigned this Aug 11, 2026

@sehaxe sehaxe left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed against opencode source + the live plugin.

Verdict: correct diagnosis, correct fix — merged (adapted).

Confirmed the root cause in opencode's session/llm/request.ts: the system prompt starts as system = [header], and after the transform hook opencode only coalesces when system.length > 2. Pushing a 2nd element yields exactly two system messages, which litellm/vLLM reject. Your curl repro against api-llm.novacom.by matches this exactly.

Applied to master with two changes:

  1. Reconciled a conflict — the branch was based on an older master. It used systemBlock() with no arg, which would have reverted the sessionID threading (systemBlock(input?.sessionID)) that the role-resume fix depends on. Kept the sessionID arg, applied your merge logic on top.
  2. Added the missing test — all existing transform tests initialize output.system = [], so they exercised the push fallback and never the merge path. Added a test asserting system.length === 1 when starting from a non-empty header.

Minor note (no action): the else output.system.push(block) branch is dead in production — opencode always passes a non-empty system (it holds the header). It's kept only so tests with system: [] still exercise the injection. Fine to keep.

60 tests green. Thank you for the fix — it directly affects the corp LiteLLM proxy.

@sehaxe

sehaxe commented Aug 15, 2026

Copy link
Copy Markdown
Owner

Merged to master (adapted, with sessionID conflict reconciled + test added). Thanks!

@sehaxe sehaxe closed this Aug 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants