mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-15 20:15:32 +00:00
fix(proxy): hoist duplicate system messages before coalescing
Claude Code sends the system prompt as the top-level `system` field and, separately, sends skill/plugin listings as `role: "system"` entries inside `messages` (#1459 made the transformer accept those). `transform()` unconditionally prepends the top-level field, so once both are present the OpenAI-compat payload ends up with two `system` messages that are not adjacent. `coalesceMessages` only merges consecutive same-role messages and explicitly skips `system`, so it cannot fix this. Strict OpenAI-compatible backends (LiteLLM among them) reject that shape with: 400 A 'system' message can only appear at index 0 of the messages array. Add `hoistSystemMessages`, run before `coalesceMessages`, which extracts every `system` message in encounter order and reinserts a single merged one at index 0. Content-preserving, no behavior change when at most one system message is present.
This commit is contained in:
1 parent
7fb56d62e1
commit
b720231077
3 files changed
+79
-4
No files matched your search
@@ -321,4 +321,32 @@ describe('ProxyRequestTransformer regressions', () => {
|
||||
|
||||
expect(result.tool_choice).toBe('auto');
|
||||
});
|
||||
|
||||
it('merges the top-level system field with a mid-array system message into one leading system message', () => {
|
||||
// Claude Code sends the main system prompt via the top-level `system`
|
||||
// field AND a skill/plugin listing as a `role: "system"` message inside
|
||||
// `messages` (see #1459). Prepending the top-level field unconditionally
|
||||
// used to leave two non-adjacent `system` messages in the payload, which
|
||||
// strict OpenAI-compatible backends (LiteLLM among them) reject with:
|
||||
// `400 A 'system' message can only appear at index 0 of the messages array.`
|
||||
const result = new ProxyRequestTransformer().transform({
|
||||
system: [{ type: 'text', text: 'You are Claude Code, a CLI tool.' }],
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content: 'The following skills are available for use with the Skill tool:\n- foo',
|
||||
},
|
||||
{ role: 'user', content: 'ping' },
|
||||
],
|
||||
});
|
||||
|
||||
const systemMessages = result.messages.filter((message) => message.role === 'system');
|
||||
expect(systemMessages).toHaveLength(1);
|
||||
expect(result.messages[0]).toEqual({
|
||||
role: 'system',
|
||||
content:
|
||||
'You are Claude Code, a CLI tool.\n\nThe following skills are available for use with the Skill tool:\n- foo',
|
||||
});
|
||||
expect(result.messages[1]).toEqual({ role: 'user', content: 'ping' });
|
||||
});
|
||||
});
|
||||
@@ -43,7 +43,7 @@ describe('ProxyRequestTransformer', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('accepts Claude Code system messages in the messages array', () => {
|
||||
it('accepts Claude Code system messages in the messages array and hoists them to a single leading system message', () => {
|
||||
const transformer = new ProxyRequestTransformer();
|
||||
const result = transformer.transform({
|
||||
messages: [
|
||||
@@ -53,10 +53,14 @@ describe('ProxyRequestTransformer', () => {
|
||||
],
|
||||
});
|
||||
|
||||
// Strict OpenAI-compatible backends (e.g. LiteLLM) reject any payload
|
||||
// where `system` is not alone at index 0, so a mid-array `system`
|
||||
// message must be hoisted rather than left in place. See #1459 for why
|
||||
// the message must be accepted at all, and the coalesce-duplicate-system
|
||||
// fix for why it can't simply stay where it landed.
|
||||
expect(result.messages).toEqual([
|
||||
{ role: 'user', content: 'hello' },
|
||||
{ role: 'system', content: 'answer tersely' },
|
||||
{ role: 'user', content: 'which model is this?' },
|
||||
{ role: 'user', content: 'hello\nwhich model is this?' },
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user