fix(llm): keep system prompts on premium chat, reject incomplete streams, stop double-charging quota
This commit is contained in:
parent
d96601a283
commit
d311e8123f
10 changed files with 1225 additions and 237 deletions
124
packages/core/__tests__/llm-chat.test.ts
Normal file
124
packages/core/__tests__/llm-chat.test.ts
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
CHAT_TRUNCATION_MARKER,
|
||||
fitChatRequest,
|
||||
LLM_PROXY_CHAT_LIMITS,
|
||||
toChatRequest,
|
||||
toRoleMessages,
|
||||
} from '../src/llm-chat'
|
||||
|
||||
describe('toChatRequest', () => {
|
||||
it('separates system messages from turns and keeps turn order', () => {
|
||||
expect(toChatRequest([
|
||||
{ role: 'system', content: 'persona' },
|
||||
{ role: 'user', content: 'hi' },
|
||||
{ role: 'assistant', content: 'hello' },
|
||||
{ role: 'user', content: 'again' },
|
||||
])).toEqual({
|
||||
system: 'persona',
|
||||
turns: [
|
||||
{ role: 'user', content: 'hi' },
|
||||
{ role: 'assistant', content: 'hello' },
|
||||
{ role: 'user', content: 'again' },
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it('joins several system messages and drops non-chat roles', () => {
|
||||
expect(toChatRequest([
|
||||
{ role: 'system', content: 'a' },
|
||||
{ role: 'tool', content: 'ignored' },
|
||||
{ role: 'system', content: 'b' },
|
||||
{ role: 'system', content: ' ' },
|
||||
{ role: 'user', content: 'q' },
|
||||
])).toEqual({ system: 'a\n\nb', turns: [{ role: 'user', content: 'q' }] })
|
||||
})
|
||||
|
||||
it('omits system when there is none', () => {
|
||||
expect(toChatRequest([{ role: 'user', content: 'q' }])).toEqual({
|
||||
turns: [{ role: 'user', content: 'q' }],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('toRoleMessages', () => {
|
||||
it('round-trips a canonical Ollama message list unchanged', () => {
|
||||
const messages = [
|
||||
{ role: 'system', content: 'persona' },
|
||||
{ role: 'user', content: 'hi' },
|
||||
{ role: 'assistant', content: 'hello' },
|
||||
]
|
||||
expect(toRoleMessages(toChatRequest(messages))).toEqual(messages)
|
||||
})
|
||||
|
||||
it('puts the system message first', () => {
|
||||
expect(toRoleMessages({ system: 's', turns: [{ role: 'user', content: 'u' }] })).toEqual([
|
||||
{ role: 'system', content: 's' },
|
||||
{ role: 'user', content: 'u' },
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('fitChatRequest', () => {
|
||||
const limits = { maxSystemChars: 50, maxMessageChars: 20, maxMessages: 3, maxTotalChars: 40 }
|
||||
|
||||
it('keeps the head of an over-long system prompt within the limit', () => {
|
||||
const system = `INSTRUCTIONS ${'x'.repeat(200)}`
|
||||
const fitted = fitChatRequest({ system, turns: [{ role: 'user', content: 'q' }] }, limits)
|
||||
expect(fitted.system?.length).toBeLessThanOrEqual(limits.maxSystemChars)
|
||||
expect(fitted.system?.startsWith('INSTRUCTIONS')).toBe(true)
|
||||
expect(fitted.system?.endsWith(CHAT_TRUNCATION_MARKER)).toBe(true)
|
||||
})
|
||||
|
||||
it('drops empty turns such as an empty assistant reply', () => {
|
||||
const fitted = fitChatRequest({
|
||||
turns: [
|
||||
{ role: 'user', content: 'q1' },
|
||||
{ role: 'assistant', content: '' },
|
||||
{ role: 'user', content: 'q2' },
|
||||
],
|
||||
}, limits)
|
||||
expect(fitted.turns).toEqual([
|
||||
{ role: 'user', content: 'q1' },
|
||||
{ role: 'user', content: 'q2' },
|
||||
])
|
||||
})
|
||||
|
||||
it('keeps the newest turns within count and size limits and never starts with assistant', () => {
|
||||
const fitted = fitChatRequest({
|
||||
turns: [
|
||||
{ role: 'user', content: 'u1' },
|
||||
{ role: 'assistant', content: 'a1' },
|
||||
{ role: 'user', content: 'u2' },
|
||||
{ role: 'assistant', content: 'a2' },
|
||||
{ role: 'user', content: 'u3' },
|
||||
],
|
||||
}, limits)
|
||||
// maxMessages=3 keeps [u2, a2, u3]
|
||||
expect(fitted.turns.map((t) => t.content)).toEqual(['u2', 'a2', 'u3'])
|
||||
|
||||
const trimmed = fitChatRequest({
|
||||
turns: [
|
||||
{ role: 'user', content: 'u'.repeat(20) },
|
||||
{ role: 'assistant', content: 'a'.repeat(15) },
|
||||
{ role: 'user', content: 'q'.repeat(20) },
|
||||
],
|
||||
}, limits)
|
||||
// total 40: [a(15), q(20)] fits, then the leading assistant turn is dropped
|
||||
expect(trimmed.turns).toEqual([{ role: 'user', content: 'q'.repeat(20) }])
|
||||
})
|
||||
|
||||
it('truncates a single turn over the per-message limit', () => {
|
||||
const fitted = fitChatRequest({ turns: [{ role: 'user', content: 'z'.repeat(100) }] }, limits)
|
||||
expect(fitted.turns[0].content.length).toBeLessThanOrEqual(limits.maxMessageChars)
|
||||
})
|
||||
|
||||
it('exposes the llm-proxy limits', () => {
|
||||
expect(LLM_PROXY_CHAT_LIMITS).toEqual({
|
||||
maxSystemChars: 8_000,
|
||||
maxMessageChars: 8_000,
|
||||
maxMessages: 40,
|
||||
maxTotalChars: 50_000,
|
||||
})
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue