fix: merge goal continuation into primary system block (closes #1)#2
Open
fank wants to merge 1 commit into
Open
fix: merge goal continuation into primary system block (closes #1)#2fank wants to merge 1 commit into
fank wants to merge 1 commit into
Conversation
The `experimental.chat.system.transform` hook previously pushed the goal continuation as a separate `role: "system"` entry. On backends that enforce the OpenAI convention of exactly one system message at index 0 (Qwen on vLLM, several Llama.cpp / Mistral templates), this produced an HTTP 400 "System message must be at the beginning." error. Merge the goal block into the existing primary system entry instead. When the system array is empty, fall back to pushing — preserving current behavior for callers that rely on goal-plugin owning the entire system prompt. Behavior on lenient backends (OpenAI, Anthropic) is unchanged: identical goal content reaches the model, just collapsed into one system message. Validated locally against opencode 1.15.11 + Qwen3.5-122B-A10B on vLLM: the exact reproducer from issue willytop8#1 (`/goal draw a ball`) goes from HTTP 400 to a successful tool-calling step with no other changes. Tests: - Existing "system transform is idempotent" still passes. - New "merges into existing system block instead of adding a second one" pins the bug fix. - New "pushes a new block when system array is empty" pins the fallback. Closes willytop8#1.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changed
experimental.chat.system.transformnow merges the goal continuation block intooutput.system[0]instead ofoutput.system.push(...)-ing a second array entry. When the system array is empty, it falls back to pushing — preserving current behavior for callers that rely on goal-plugin owning the entire system prompt.Why
The previous
pushpattern produced two consecutiverole: "system"messages on the wire. Backends that enforce the OpenAI convention of exactly one system message at index 0 reject this with HTTP 400"System message must be at the beginning.". Affected backends include:raise_exceptionon a second system message)OpenAI's and Anthropic's hosted APIs silently tolerate multiple system messages, which is why this bug is invisible to users on those providers.
The opencode community has documented that plugins using
experimental.chat.system.transformshould merge rather than push (see anomalyco/opencode#23660 and anomalyco/opencode#15059). This change brings goal-plugin in line with that guidance.How
Verification
Checks
Manual OpenCode smoke test
Against opencode 1.15.11 with this plugin, provider
@ai-sdk/openai-compatiblepointing at vLLM 0.19.x serving Qwen3.5-122B-A10B (same setup as issue #1):push){"message":"System message must be at the beginning.","type":"BadRequestError","code":400}/goal draw a ballsucceeds; model executeswritetool, creates the artifact, step finishes cleanly withreason: "tool-calls", goal auto-continues to next iterationToken usage in the passing case: 9120 input / 283 output — confirming both the base system prompt and the goal continuation block reach the model, just collapsed into a single
role: "system"message instead of two.Tests
"system transform is idempotent"— unchanged, still passes (idempotence guard preserved)."merges into existing system block instead of adding a second one"— pins the bug fix; assertsoutput.system.length === 1and the base prompt is preserved as a prefix."pushes a new block when system array is empty"— pins the empty-array fallback.Related
role: systemmessage breaks strict-template backends (Qwen vLLM and similar) #1 in this repo (filed alongside this fix)Closes #1.