mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-14 02:15:36 +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
@@ -671,6 +671,49 @@ function transformMessages(messagesValue: unknown): OpenAIMessage[] {
|
||||
return translatedMessages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hoist every `role: "system"` message to a single leading system message.
|
||||
*
|
||||
* Claude Code sends the system prompt as the top-level `system` field *and*,
|
||||
* separately, sends skill/plugin listings as `role: "system"` entries inside
|
||||
* `messages` (see #1459). `ProxyRequestTransformer.transform` prepends the
|
||||
* top-level `system` field unconditionally, so once both are present the
|
||||
* resulting array holds two `system` messages that are not adjacent —
|
||||
* `coalesceMessages` only merges *consecutive* same-role messages, so it
|
||||
* cannot fix this case even if it did coalesce `system` (which it explicitly
|
||||
* excludes below).
|
||||
*
|
||||
* Strict OpenAI-compatible backends (LiteLLM among them) reject any request
|
||||
* where a `system` message is not alone at index 0:
|
||||
* `400 A 'system' message can only appear at index 0 of the messages array.`
|
||||
*
|
||||
* This pass extracts all `system` messages in encounter order, joins their
|
||||
* content with a blank line, and reinserts the result as the sole leading
|
||||
* message — content-preserving, order-preserving for everything else.
|
||||
*/
|
||||
function hoistSystemMessages(messages: OpenAIMessage[]): OpenAIMessage[] {
|
||||
const systemParts: string[] = [];
|
||||
const rest: OpenAIMessage[] = [];
|
||||
|
||||
for (const message of messages) {
|
||||
if (message.role !== 'system') {
|
||||
rest.push(message);
|
||||
continue;
|
||||
}
|
||||
const content = message.content;
|
||||
const text = typeof content === 'string' ? content : '';
|
||||
if (text.trim().length > 0) {
|
||||
systemParts.push(text);
|
||||
}
|
||||
}
|
||||
|
||||
if (systemParts.length === 0) {
|
||||
return rest;
|
||||
}
|
||||
|
||||
return [{ role: 'system', content: systemParts.join('\n\n') }, ...rest];
|
||||
}
|
||||
|
||||
/**
|
||||
* Coalesce consecutive messages of the same role.
|
||||
* OpenAI/vLLM/Ollama/Mistral require strict user<->assistant alternation.
|
||||
@@ -741,7 +784,7 @@ export class ProxyRequestTransformer {
|
||||
// was billed. See:
|
||||
// https://platform.openai.com/docs/api-reference/chat-streaming
|
||||
...(source.stream === true ? { stream_options: { include_usage: true } } : {}),
|
||||
messages: coalesceMessages(allMessages),
|
||||
messages: coalesceMessages(hoistSystemMessages(allMessages)),
|
||||
max_tokens: asNumber(source.max_tokens),
|
||||
temperature: asNumber(source.temperature),
|
||||
top_p: asNumber(source.top_p),
|
||||
|
||||
Reference in new issue
Block a user