char-fork/llama.cpp

git clone https://git.t4t.associates/char-fork/llama.cpp

Aleksander Grygierui: Linting & Formatting scripts (#26819)92d1bb0c9

char/aliceai-t5-moe
3.5 KiB97 linesraw
1import { MessageRole } from '$lib/enums';
2import type { DatabaseMessage } from '$lib/types/database';
3import { deriveAgenticSections } from '$lib/utils/agentic';
4import { describe, expect, it } from 'vitest';
5
6function makeAssistant(overrides: Partial<DatabaseMessage> = {}): DatabaseMessage {
7	return {
8		children: [],
9		content: overrides.content ?? '',
10		convId: 'conv-1',
11		id: overrides.id ?? 'ast-1',
12		parent: null,
13		role: MessageRole.ASSISTANT,
14		timestamp: Date.now(),
15		type: 'text',
16		...overrides
17	} as DatabaseMessage;
18}
19
20// Mirrors the filter inside ChatService.convertDbMessageToApiChatMessageData:
21// a partial tool call captured mid-stream must not survive into the next request
22// payload. The fix in chatStore.savePartialResponseIfNeeded clears toolCalls to ''
23// on Stop/Send immediately, mirroring what the agentic flow already does in
24// onAssistantTurnComplete(...undefined).
25function buildApiToolCalls(message: DatabaseMessage): unknown[] | undefined {
26	if (!message.toolCalls) return undefined;
27
28	try {
29		const parsed = JSON.parse(message.toolCalls);
30
31		return Array.isArray(parsed) && parsed.length > 0 ? parsed : undefined;
32	} catch {
33		return undefined;
34	}
35}
36
37describe('partial tool call cleanup', () => {
38	// Reproduces the broken payload from the user's screenshot: model was
39	// streaming a tool call whose arguments JSON was cut mid-string. The outer
40	// envelope still parses, but the arguments themselves are invalid JSON and
41	// the server rejects the request.
42	it('marks a partial tool call payload as unsafe to re-send', () => {
43		const message = makeAssistant({
44			content: 'partial reasoning',
45			toolCalls: JSON.stringify([
46				{
47					function: {
48						arguments: '{"command":`grep -n \\"read_to\\" ` /Users',
49						name: 'exec_shell_command'
50					},
51					id: 'call_1',
52					type: 'function'
53				}
54			])
55		});
56		const apiToolCalls = buildApiToolCalls(message);
57
58		// The bug: even though arguments are invalid, the outer array parses and
59		// the request gets sent. Function arguments must be parseable JSON on their
60		// own for the server to execute the tool.
61		expect(apiToolCalls).toBeDefined();
62		const args = (apiToolCalls![0] as { function: { arguments: string } }).function.arguments;
63
64		expect(() => JSON.parse(args)).toThrow();
65	});
66
67	// After Stop, savePartialResponseIfNeeded clears toolCalls and the agentic
68	// flow does the same in its silent-return detection. The next request reads
69	// toolCalls = '' and the conversion drops the field entirely so the server
70	// never sees the half-streamed call.
71	it('drops tool_calls from the API request after toolCalls is cleared', () => {
72		const clearedMessage = makeAssistant({
73			content: 'partial reasoning',
74			toolCalls: ''
75		});
76		const apiToolCalls = buildApiToolCalls(clearedMessage);
77
78		expect(apiToolCalls).toBeUndefined();
79	});
80
81	// The cleanup path keeps the partial reasoning content visible in the UI;
82	// only the tool_calls field is reset. deriveAgenticSections should still
83	// surface the reasoning as interrupted (no content / no tool calls behind
84	// it) without resurrecting the dead tool call block.
85	it('keeps reasoning content visible after cleanup, without a tool call block', () => {
86		const cleared = makeAssistant({
87			content: '',
88			reasoningContent: 'thinking about read_to',
89			toolCalls: ''
90		});
91		const sections = deriveAgenticSections(cleared);
92
93		expect(sections).toHaveLength(1);
94		expect(sections[0].type).toBe('reasoning');
95		expect(sections.some((s) => s.type.includes('tool_call'))).toBe(false);
96	});
97});