fix(llm): keep system prompts on premium chat, reject incomplete streams, stop double-charging quota

This commit is contained in:
Yun Chan 2026-09-28 00:53:44 +09:00
parent d96601a283
commit d311e8123f
10 changed files with 1225 additions and 237 deletions

View 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,
})
})
})