Skip to content

fix(opencode): coalesce system messages for OpenAI-compatible providers - #42801

Open
ondrejch wants to merge 1 commit into
anomalyco:devfrom
ondrejch:fix/openai-compatible-system-messages
Open

fix(opencode): coalesce system messages for OpenAI-compatible providers#42801
ondrejch wants to merge 1 commit into
anomalyco:devfrom
ondrejch:fix/openai-compatible-system-messages

Conversation

@ondrejch

Copy link
Copy Markdown

Issue for this PR

Fixes#34321
Related: #15059, #23656, #34322, #38671

Type of change

  • Bug fix

What does this PR do?

Plugins can append system instructions through
experimental.chat.system.transform, producing multiple system messages.

OpenAI-compatible local backends such as SGLang/vLLM serving Qwen reject
that message shape with System message must be at the beginning.

This coalesces system blocks only for @ai-sdk/openai-compatible before
dispatch. Other providers retain their existing multi-system behavior,
including Anthropic prompt-cache boundaries.

Unlike #38671, this does not globally merge adjacent system messages.

How did you verify your code works?

  • Added regression coverage for OpenAI-compatible system coalescing.
  • Added regression coverage that Anthropic keeps separate system messages.
  • bun test test/provider/transform.test.ts from packages/opencode
  • bun typecheck from packages/opencode

Screenshots / recordings

If this is a UI change, please include a screenshot or recording.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

@github-actions

Copy link
Copy Markdown
Contributor

The following comment was made by an LLM, it may be inaccurate:

Potential Related PRs Found

PR #38671 - fix(opencode): collapse plugin-pushed system messages into one

PR #39905 - feat(opencode): add system prompt debug command

  • Relates to system message handling and debugging, though not a direct duplicate.

PR #19961 - fix(session): fire system.transform before messages.transform

  • Related to the system transformation pipeline that your PR depends on.

PR #16981 - fix: handle system messages for non-Anthropic providers

  • Similar in spirit to your fix, addressing provider-specific system message handling.

The most significant one is #38671, which you've already acknowledged in your PR description as a related prior approach to this problem.

@matchaxnbmatchaxnb left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I like the idea of the change but think it'd be better in 2 parts, one here to add the wiring for the mechanism and another in @ai-sdk/openai-compatible to perform the transformation.

),
...input.messages,
const systemMessages =
input.model.api.npm === "@ai-sdk/openai-compatible" && system.length > 1

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Wouldn't it be cleaner to include an optional system transformer principle in @ai-sdk/openai-compatible and to apply it?

It would be null for the other providers and would make this code generic as it would be sufficient to apply the pipeline.

Here you're introducing a bit of spaghetti, while this should definitely be part of the OpenAI compatible sauce.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I kept this in LLMRequestPrep on purpose. @ai-sdk/openai-compatible is an upstream Vercel package; OpenCode only patches it for SDK bugs. The join is OpenCode policy (plugin system.transform + backends that reject multiple system messages), so it belongs next to the other model.api.npm request shaping, not in the SDK.

A generic transformer registry would also change Anthropic, which we are deliberately not doing (prompt-cache boundaries). Happy to move the join into ProviderTransform.message if a maintainer prefers that existing hook.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

my understanding is that it would not, because by default the "transformer" (not the best word i suppose with the domain we're working on) would be a map function that would default to being undefined.

But i see code just before already has openai-specific quirks to it so your change is fine by me although it's not to my taste. IANAM :-)

@Enough1122

Copy link
Copy Markdown

AI code review — automated review for reference; please use your judgment.

Scope: coalesces multiple system messages into one only for @ai-sdk/openai-compatible providers, targeting local backends (SGLang/vLLM/Qwen) that reject system-after-system shapes.

  • Right scoping: gating on model.api.npm === "@ai-sdk/openai-compatible" keeps Anthropic prompt-cache boundaries and other providers' multi-system behavior intact, and the regression test pins both sides of that.
  • The system.length > 1 guard means the common single-system path stays byte-identical — nice touch avoiding unnecessary request-shape churn.
  • Nit: blocks are joined with a single "\n". If a plugin's block doesn't end with a newline, adjacent instructions can run together mid-line; "\n\n" would be safer as a separator.
  • The isOpenaiOauth || input.isWorkflow passthrough paths are correctly untouched.
  • Tests lean heavily on as any casts for model/session fixtures; acceptable for now, but they'll age faster than typed fixtures if Model.Info shape evolves.

Sign up for freeto 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.

Multiple system messages break OpenAI-compatible providers when plugins use system.transform

3 participants

@ondrejch@Enough1122@matchaxnb