mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-18 04:14:06 +00:00
chore: merge released dev into PR #1690
This commit is contained in:
commit
59eec74f40
4 files changed
+151
-5
No files matched your search
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@kaitranntt/ccs",
|
"name": "@kaitranntt/ccs",
|
||||||
"version": "8.8.1-dev.16",
|
"version": "8.8.1-dev.17",
|
||||||
"description": "Claude Codex Switch - Instant profile switching between Claude, GLM, Kimi, and more",
|
"description": "Claude Codex Switch - Instant profile switching between Claude, GLM, Kimi, and more",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"cli",
|
"cli",
|
||||||
|
|||||||
@@ -671,6 +671,52 @@ function transformMessages(messagesValue: unknown): OpenAIMessage[] {
|
|||||||
return translatedMessages;
|
return translatedMessages;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hoist every accepted `role: "system"` message to one 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.`
|
||||||
|
*
|
||||||
|
* Inline system messages may appear between complete turns, including after
|
||||||
|
* tool results. They may not interrupt a pending assistant tool-call/result
|
||||||
|
* sequence; `transformMessages` rejects that ambiguous placement before this
|
||||||
|
* pass. Accepted system messages are extracted in encounter order, joined with
|
||||||
|
* a blank line, and reinserted as the sole leading message. Everything else
|
||||||
|
* keeps its relative order before normal same-role coalescing.
|
||||||
|
*/
|
||||||
|
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.
|
* Coalesce consecutive messages of the same role.
|
||||||
* OpenAI/vLLM/Ollama/Mistral require strict user<->assistant alternation.
|
* OpenAI/vLLM/Ollama/Mistral require strict user<->assistant alternation.
|
||||||
@@ -741,7 +787,7 @@ export class ProxyRequestTransformer {
|
|||||||
// was billed. See:
|
// was billed. See:
|
||||||
// https://platform.openai.com/docs/api-reference/chat-streaming
|
// https://platform.openai.com/docs/api-reference/chat-streaming
|
||||||
...(source.stream === true ? { stream_options: { include_usage: true } } : {}),
|
...(source.stream === true ? { stream_options: { include_usage: true } } : {}),
|
||||||
messages: coalesceMessages(allMessages),
|
messages: coalesceMessages(hoistSystemMessages(allMessages)),
|
||||||
max_tokens: asNumber(source.max_tokens),
|
max_tokens: asNumber(source.max_tokens),
|
||||||
temperature: asNumber(source.temperature),
|
temperature: asNumber(source.temperature),
|
||||||
top_p: asNumber(source.top_p),
|
top_p: asNumber(source.top_p),
|
||||||
|
|||||||
@@ -321,4 +321,100 @@ describe('ProxyRequestTransformer regressions', () => {
|
|||||||
|
|
||||||
expect(result.tool_choice).toBe('auto');
|
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: 'user', content: 'ping' },
|
||||||
|
{
|
||||||
|
role: 'system',
|
||||||
|
content: 'The following skills are available for use with the Skill tool:\n- foo',
|
||||||
|
},
|
||||||
|
{ role: 'user', content: 'pong' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
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\npong' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('hoists a late system message after complete parallel tool results without disturbing tool order', () => {
|
||||||
|
const result = new ProxyRequestTransformer().transform({
|
||||||
|
system: 'base instructions',
|
||||||
|
messages: [
|
||||||
|
{ role: 'user', content: 'inspect both files' },
|
||||||
|
{
|
||||||
|
role: 'assistant',
|
||||||
|
content: [
|
||||||
|
{ type: 'tool_use', id: 'toolu_1', name: 'read', input: { path: 'a.ts' } },
|
||||||
|
{ type: 'tool_use', id: 'toolu_2', name: 'read', input: { path: 'b.ts' } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: 'user',
|
||||||
|
content: [
|
||||||
|
{ type: 'tool_result', tool_use_id: 'toolu_1', content: 'a contents' },
|
||||||
|
{ type: 'tool_result', tool_use_id: 'toolu_2', content: 'b contents' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ role: 'system', content: 'late instructions' },
|
||||||
|
{ role: 'user', content: 'compare them' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.messages).toEqual([
|
||||||
|
{ role: 'system', content: 'base instructions\n\nlate instructions' },
|
||||||
|
{ role: 'user', content: 'inspect both files' },
|
||||||
|
{
|
||||||
|
role: 'assistant',
|
||||||
|
content: '',
|
||||||
|
tool_calls: [
|
||||||
|
{
|
||||||
|
id: 'toolu_1',
|
||||||
|
type: 'function',
|
||||||
|
function: { name: 'read', arguments: '{"path":"a.ts"}' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'toolu_2',
|
||||||
|
type: 'function',
|
||||||
|
function: { name: 'read', arguments: '{"path":"b.ts"}' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ role: 'tool', tool_call_id: 'toolu_1', content: 'a contents' },
|
||||||
|
{ role: 'tool', tool_call_id: 'toolu_2', content: 'b contents' },
|
||||||
|
{ role: 'user', content: 'compare them' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects a system message inserted before pending tool results', () => {
|
||||||
|
expect(() =>
|
||||||
|
new ProxyRequestTransformer().transform({
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: 'assistant',
|
||||||
|
content: [{ type: 'tool_use', id: 'toolu_1', name: 'read', input: { path: 'a.ts' } }],
|
||||||
|
},
|
||||||
|
{ role: 'system', content: 'interrupting instructions' },
|
||||||
|
{
|
||||||
|
role: 'user',
|
||||||
|
content: [{ type: 'tool_result', tool_use_id: 'toolu_1', content: 'a contents' }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
).toThrow('role must be "user" with tool_result blocks after assistant tool_use');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -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 transformer = new ProxyRequestTransformer();
|
||||||
const result = transformer.transform({
|
const result = transformer.transform({
|
||||||
messages: [
|
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([
|
expect(result.messages).toEqual([
|
||||||
{ role: 'user', content: 'hello' },
|
|
||||||
{ role: 'system', content: 'answer tersely' },
|
{ 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