fix(sse): relocate directive-only messages off messages[0] - #10457
Merged
diegosouzapw merged 1 commit intoAug 15, 2026
Merged
Conversation
The upstream Messages API rejects directive-style messages (empty content array with a message-level output_config) when they sit at messages[0] — the initial system prompt position — while accepting the form at any other position. Measured in production: 122x 400 on the offical-claude combo in one hour. The mid-conversation-system passthrough (official provider + 1M-context beta models) keeps system-role messages inside messages[], so a directive that arrived first went upstream unchanged. relocateDirectiveOnlyMessages() moves the whole leading run of empty system messages: directive-only ones past the first real turn, plain empties dropped. extractSystemRoleMessages() now folds a directive's output_config into the top-level parameter instead of silently discarding it. Signed-off-by: Minxi Hou <houminxi@gmail.com>
Owner
|
Thanks @HouMinXi — this is a genuine 400 regression (122/hour on the Claude combo) and the callback is thorough: the relocation handles leading runs, null-safety, developer-role directives, and the no-real-turn fold to top-level, all backed by 12 unit tests plus an integration pass-through. Excellent work. Note the red CI is an inherited base-red (hung unit suite), not this change. 🚀 |
diegosouzapw
merged commit Aug 15, 2026
8ff7f7d
into
diegosouzapw:release/v3.8.50
15 of 16 checks passed
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.
Problem: Anthropic rejects Claude Code-style directive messages when they land at
messages[0]. Measured in production on 2026-08-15: 122x400in a single hour on theoffical-claudecombo (fleet-clientskey,claude-opus-5), all with the same upstream error:Claude Code 2.1.154+ clients send directives as system-role messages with an empty content array and a message-level
output_config. The mid-conversation-system passthrough (provider === "claude"+ 1M-context beta models,shouldUseMidConversationSystem) deliberately keeps system-role messages insidemessages[], so a directive that arrived first went upstream unchanged and 400'd. The other passthrough path (extractSystemRoleMessages) lifted the message away but silently dropped the directive'soutput_config, losing the client's output-format configuration.Fixes:
open-sse/handlers/chatCore/claudeSystemRole.ts— newrelocateDirectiveOnlyMessages(): collects the whole leading run of empty system/developer messages and relocates the directive-only ones (empty content array + message-leveloutput_config) past the first real (user/assistant) turn, in order, where Anthropic accepts the form ("accepted at any position"). Plain empty system messages carry nothing and are dropped. When the conversation has no real turn, the first directive'soutput_configis folded into the top-level parameter (an explicit top-level value wins) and the run is dropped.extractSystemRoleMessages()— no longer silently discards a directive's message-leveloutput_config; it is folded into the top-level parameter whether or not the message also carried text blocks (an explicit top-level value wins; among several directives the first wins).open-sse/handlers/chatCore.ts— the mid-conversation-system passthrough now callsrelocateDirectiveOnlyMessages().Verification: new
tests/unit/claude-directive-only-relocation.test.ts(16 tests: relocation, consecutive directives, empty-system+directive runs, walking past text system messages, developer-role directives, the no-real-turn fallback, top-level precedence, no-op cases) andtests/unit/claude-directive-midconv-passthrough.test.ts(ahandleChatCoreintegration test with a mocked fetch asserting the directive never reaches upstream atmessages[0]). Bug-injection proven at each layer: disabling the extract-fold made the preserve test fail; disabling the relocation made the move tests fail; removing the chatCore call made the integration test fail; reverting restores green. Regression suites:system-role-extraction,claude-system-role-cache-boundary,g13-combo-chatcore-golden,agentrouter-chatcore-protocols,claude-code-compatible-request,claude-code-compatible-helpers,cc-compatible-provider,claude-helper-minimax-output-config— 104/104.npm run typecheck:coreclean, ESLint clean on all touched files.CodeQLandBuild Docker (linux/arm64, linux/amd64)failing on the upstream tree itself (run https://github.com/diegosouzapw/OmniRoute/actions/runs/31858514611) — unrelated to this change.