Skip to content

fix(sse): hoist leading text system messages on the Claude mid-conversation-system passthrough (Anthropic 400) - #13072

Merged
diegosouzapw merged 1 commit into
diegosouzapw:mainfrom
JasonBroderick:fix/hoist-leading-system-messages
Sep 9, 2026
Merged

diegosouzapw merged 1 commit into
diegosouzapw:mainfrom
JasonBroderick:fix/hoist-leading-system-messages

Conversation

@JasonBroderick

Copy link
Copy Markdown
Contributor

What

On the Claude Code semantic passthrough path (provider claude, 1M-context model, system + tools present), system-role messages are kept inside messages[] and only directive-only messages are relocated off messages[0]. Anthropic rejects a text-bearing system message at messages[0] as well:

messages.0: use the top-level 'system' parameter for the initial system prompt; the directive-only form (content: [] with output_config) is accepted at any position

That is exactly where the Output Styles injection lands (outputStyles/apply.ts unshifts a system message), so with an output style active every Claude Code turn on such a model returns 400.

Fix

hoistLeadingTextSystemMessages() in open-sse/handlers/chatCore/claudeSystemRole.ts: move only the leading run of text-bearing system-role messages (everything before the first user/assistant turn) into the top-level system parameter, called before relocateDirectiveOnlyMessages() on that path. Genuine mid-conversation system turns keep their position and cache prefix; directive-only messages in the leading run are left to the existing relocation. An existing top-level system (string or blocks) is preserved and the hoisted blocks are appended.

Tests

tests/unit/claude-leading-text-system-hoist.test.ts: injected-style case, string top-level system, mixed directive/text leading run (combined with the relocation), and the no-op case. The existing directive-only and mid-conversation passthrough tests still pass.

Repro

POST /v1/messages with Claude Code client headers (user-agent: claude-cli/..., x-app: cli), model claude-opus-5, a top-level system, one tool, and messages[0] = {role: "system", content: "[OmniRoute Output Styles] ..."} followed by a user turn. Before: 400 from Anthropic. After: 200. Running on a self-hosted instance since 2026-09-04.

🤖 Generated with Claude Code

…sation-system passthrough

On the Claude Code semantic passthrough with a 1M-context model (system + tools
present), system-role messages are deliberately kept inside messages[] and only
directive-only messages (content: [] + output_config) are relocated off
messages[0]. Anthropic also rejects a text-bearing system message at messages[0]:

    messages.0: use the top-level 'system' parameter for the initial system
    prompt; the directive-only form (content: [] with output_config) is
    accepted at any position

That is exactly where the Output Styles injection lands
(open-sse/services/compression/outputStyles/apply.ts unshifts a system message),
so every Claude Code turn with an output style active on such a model fails
with 400.

Add hoistLeadingTextSystemMessages(): move only the leading run of text-bearing
system-role messages (everything before the first user/assistant turn) into the
top-level system parameter, and call it before relocateDirectiveOnlyMessages()
on that path. Genuine mid-conversation system turns keep their position and
cache prefix; directive-only messages in the run are left for the existing
relocation. Unit tests cover the injected-style case, the string top-level
system case, mixed directive/text runs, and the no-op case.

Repro: POST /v1/messages with Claude Code client headers (user-agent
claude-cli/..., x-app: cli), model claude-opus-5, a top-level system, one tool,
and messages[0] = {role: "system", content: "[OmniRoute Output Styles] ..."}.
Before: 400 from Anthropic. After: 200.
@diegosouzapw
diegosouzapw merged commit a3ca33f into diegosouzapw:main Sep 9, 2026
3 checks passed
diegosouzapw added a commit to onexer88-blip/OmniRoute that referenced this pull request Sep 24, 2026
…souzapw#13887 fix

Restore every file except accountFallback.ts and its test to the release tip:
the branch carried main-only content (diegosouzapw#13072 hoistLeadingTextSystemMessages
in chatCore.ts and its test, model-catalog and usage-ordering changes) that
the release does not have and that broke the merge train.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
diegosouzapw added a commit that referenced this pull request Sep 29, 2026
Brings the 12 main-only commits into the frozen release branch so the release
PR (#11442) is no longer CONFLICTING. Most are main twins of fixes the release
branch already carries in a newer shape; resolved by proving the release side
contains them:
- #12246 temp-dir retry: release uses cleanupTempDataDir / maxRetries (kept ours).
- #11719 Bun image: release is already on oven/bun 1.4.2 (kept ours).
- #12086 CVE-2025-68121 .trivyignore: dropped on release by #12429 (wreq-js).
- #13865 npm 11 / electron overrides: release pins are newer (kept ours);
  the optional transformers webpack-ignore change merged cleanly.
- streamTiming clock injection: release moved to a monotonic clock (kept ours).
Real main-only fix ported: #13072 hoistLeadingTextSystemMessages, added to the
release's claudeSystemRole.ts (the call site in chatCore.ts merged cleanly).
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