Skip to content

Commit b1deb69

Browse files
committed
feat: sanitize titles by stripping <think> markers across multiple components and endpoints
1 parent 7f8e2ea commit b1deb69

File tree

11 files changed

+96
-66
lines changed

11 files changed

+96
-66
lines changed

scira-chat

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Subproject commit 892ec4a60d23cd1c19b1a079c799d24a8a79a262

src/lib/components/chat/OpenReasoningResults.svelte

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818

1919
<details
2020
bind:open={isOpen}
21-
class="group flex w-fit max-w-full flex-col rounded-xl border border-gray-200 bg-white shadow-sm dark:border-gray-800 dark:bg-gray-900"
21+
class="group mb-4 flex w-fit max-w-full flex-col rounded-xl border border-gray-200 bg-white shadow-sm dark:border-gray-800 dark:bg-gray-900"
2222
>
2323
<summary
2424
class="
@@ -69,7 +69,7 @@
6969
</summary>
7070

7171
<div
72-
class="space-y-4 border-t border-gray-200 px-5 pb-2 pt-2 text-sm text-gray-600 dark:border-gray-800 dark:text-gray-400"
72+
class="space-y-4 border-t border-gray-200 p-3 text-sm text-gray-600 dark:border-gray-800 dark:text-gray-400"
7373
>
7474
{#key content}
7575
<MarkdownRenderer {content} />

src/lib/server/api/routes/groups/conversations.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -172,9 +172,12 @@ export const conversationGroup = new Elysia().use(authPlugin).group("/conversati
172172
}
173173
}
174174

175-
// Only include defined values in the update
175+
176+
// Only include defined values in the update (sanitize title)
176177
const updateValues = {
177-
...(body.title !== undefined && { title: body.title }),
178+
...(body.title !== undefined && {
179+
title: body.title.replace(/<\/?think>/gi, "").trim(),
180+
}),
178181
...(body.model !== undefined && { model: body.model }),
179182
};
180183

src/lib/server/generateFromDefaultEndpoint.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,30 @@
1-
import { taskModel } from "$lib/server/models";
1+
import { taskModel, models } from "$lib/server/models";
22
import { MessageUpdateType, type MessageUpdate } from "$lib/types/MessageUpdate";
33
import type { EndpointMessage } from "./endpoints/endpoints";
44

55
export async function* generateFromDefaultEndpoint({
66
messages,
77
preprompt,
88
generateSettings,
9+
modelId,
910
}: {
1011
messages: EndpointMessage[];
1112
preprompt?: string;
1213
generateSettings?: Record<string, unknown>;
14+
/** Optional: use this model instead of the default task model */
15+
modelId?: string;
1316
}): AsyncGenerator<MessageUpdate, string, undefined> {
1417
try {
15-
const endpoint = await taskModel.getEndpoint();
18+
// Choose endpoint based on provided modelId, else fall back to taskModel
19+
const model = modelId ? (models.find((m) => m.id === modelId) ?? taskModel) : taskModel;
20+
const endpoint = await model.getEndpoint();
1621
const tokenStream = await endpoint({ messages, preprompt, generateSettings });
1722

1823
for await (const output of tokenStream) {
1924
// if not generated_text is here it means the generation is not done
2025
if (output.generated_text) {
2126
let generated_text = output.generated_text;
22-
for (const stop of [...(taskModel.parameters?.stop ?? []), "<|endoftext|>"]) {
27+
for (const stop of [...(model.parameters?.stop ?? []), "<|endoftext|>"]) {
2328
if (generated_text.endsWith(stop)) {
2429
generated_text = generated_text.slice(0, -stop.length).trimEnd();
2530
}

src/lib/server/textGeneration/generate.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ Do not use prefixes such as Response: or Answer: when answering to the user.`,
9999
generateSettings: {
100100
max_new_tokens: 1024,
101101
},
102+
modelId: model.id,
102103
});
103104
finalAnswer = summary;
104105
yield {
@@ -210,7 +211,7 @@ Do not use prefixes such as Response: or Answer: when answering to the user.`,
210211
) {
211212
lastReasoningUpdate = new Date();
212213
try {
213-
generateSummaryOfReasoning(reasoningBuffer).then((summary) => {
214+
generateSummaryOfReasoning(reasoningBuffer, model.id).then((summary) => {
214215
status = summary;
215216
});
216217
} catch (e) {

src/lib/server/textGeneration/reasoning.ts

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import { generateFromDefaultEndpoint } from "../generateFromDefaultEndpoint";
2-
import { taskModel } from "../models";
32
import { getReturnFromGenerator } from "$lib/utils/getReturnFromGenerator";
43
import { logger } from "../logger";
54

6-
export async function generateSummaryOfReasoning(buffer: string): Promise<string> {
7-
let summary: string | undefined;
5+
export async function generateSummaryOfReasoning(buffer: string, modelId?: string): Promise<string> {
6+
let summary: string | undefined;
87

98
const messages = [
109
{
@@ -18,24 +17,25 @@ The text might be incomplete, try your best to summarize it in one very short se
1817

1918
// Tools removed: no tool-based summarization path
2019

21-
if (!summary) {
22-
summary = await getReturnFromGenerator(
23-
generateFromDefaultEndpoint({
24-
messages: [
25-
{
26-
from: "user",
27-
content: buffer.slice(-300),
28-
},
29-
],
30-
preprompt: `You are tasked with summarizing the latest reasoning steps. Never describe results of the reasoning, only the process. Remain vague in your summary.
20+
if (!summary) {
21+
summary = await getReturnFromGenerator(
22+
generateFromDefaultEndpoint({
23+
messages: [
24+
{
25+
from: "user",
26+
content: buffer.slice(-300),
27+
},
28+
],
29+
preprompt: `You are tasked with summarizing the latest reasoning steps. Never describe results of the reasoning, only the process. Remain vague in your summary.
3130
The text might be incomplete, try your best to summarize it in one very short sentence, starting with a gerund and ending with three points.
3231
Example: "Thinking about life...", "Summarizing the results...", "Processing the input..."`,
33-
generateSettings: {
34-
max_new_tokens: 50,
35-
},
36-
})
37-
);
38-
}
32+
generateSettings: {
33+
max_new_tokens: 50,
34+
},
35+
modelId,
36+
})
37+
);
38+
}
3939

4040
if (!summary) {
4141
return "Reasoning...";

src/lib/server/textGeneration/title.ts

Lines changed: 29 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { logger } from "$lib/server/logger";
44
import { MessageUpdateType, type MessageUpdate } from "$lib/types/MessageUpdate";
55
import type { Conversation } from "$lib/types/Conversation";
66
import { getReturnFromGenerator } from "$lib/utils/getReturnFromGenerator";
7-
import { taskModel } from "../models";
7+
// taskModel no longer used directly here; we pass the current model instead
88

99
export async function* generateTitleForConversation(
1010
conv: Conversation
@@ -15,7 +15,7 @@ export async function* generateTitleForConversation(
1515
if (conv.title !== "New Chat" || !userMessage) return;
1616

1717
const prompt = userMessage.content;
18-
const title = (await generateTitle(prompt)) ?? "New Chat";
18+
const title = (await generateTitle(prompt, conv.model)) ?? "New Chat";
1919

2020
yield {
2121
type: MessageUpdateType.Title,
@@ -26,32 +26,40 @@ export async function* generateTitleForConversation(
2626
}
2727
}
2828

29-
export async function generateTitle(prompt: string) {
30-
if (config.LLM_SUMMARIZATION !== "true") {
31-
return prompt.split(/\s+/g).slice(0, 5).join(" ");
32-
}
29+
export async function generateTitle(prompt: string, modelId?: string) {
30+
if (config.LLM_SUMMARIZATION !== "true") {
31+
return prompt.split(/\s+/g).slice(0, 5).join(" ");
32+
}
3333

3434
// Tools removed: no tool-based title path
3535

36-
return await getReturnFromGenerator(
37-
generateFromDefaultEndpoint({
38-
messages: [{ from: "user", content: prompt }],
39-
preprompt:
40-
"You are a summarization AI. Summarize the user's request into a single short sentence of four words or less. Do not try to answer it, only summarize the user's query. Always start your answer with an emoji relevant to the summary",
41-
generateSettings: {
42-
max_new_tokens: 30,
43-
},
44-
})
45-
)
36+
return await getReturnFromGenerator(
37+
generateFromDefaultEndpoint({
38+
messages: [{ from: "user", content: prompt }],
39+
preprompt:
40+
"You are a summarization AI. Summarize the user's request into a single short sentence of four words or less. Do not try to answer it, only summarize the user's query. Always start your answer with an emoji relevant to the summary",
41+
generateSettings: {
42+
max_new_tokens: 30,
43+
},
44+
modelId,
45+
})
46+
)
4647
.then((summary) => {
47-
// add an emoji if none is found in the first three characters
48-
if (!/\p{Emoji}/u.test(summary.slice(0, 3))) {
49-
return "💬 " + summary;
48+
const firstFive = prompt.split(/\s+/g).slice(0, 5).join(" ");
49+
const trimmed = summary.trim();
50+
// Fallback: if empty, use emoji + first five words
51+
if (!trimmed) {
52+
return "💬 " + firstFive;
53+
}
54+
// Ensure emoji prefix if missing
55+
if (!/\p{Emoji}/u.test(trimmed.slice(0, 3))) {
56+
return "💬 " + trimmed;
5057
}
51-
return summary;
58+
return trimmed;
5259
})
5360
.catch((e) => {
5461
logger.error(e);
55-
return null;
62+
const firstFive = prompt.split(/\s+/g).slice(0, 5).join(" ");
63+
return "💬 " + firstFive;
5664
});
5765
}

src/routes/+layout.svelte

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -78,19 +78,21 @@
7878
});
7979
}
8080
81-
async function editConversationTitle(id: string, title: string) {
82-
client
83-
.conversations({ id })
84-
.patch({ title })
85-
.then(handleResponse)
86-
.then(async () => {
87-
conversations = conversations.map((conv) => (conv.id === id ? { ...conv, title } : conv));
88-
})
89-
.catch((err) => {
90-
console.error(err);
91-
$error = String(err);
92-
});
93-
}
81+
async function editConversationTitle(id: string, title: string) {
82+
// Always strip <think> markers before saving and updating UI
83+
const cleanTitle = title.replace(/<\/?think>/gi, "").trim();
84+
client
85+
.conversations({ id })
86+
.patch({ title: cleanTitle })
87+
.then(handleResponse)
88+
.then(async () => {
89+
conversations = conversations.map((conv) => (conv.id === id ? { ...conv, title: cleanTitle } : conv));
90+
})
91+
.catch((err) => {
92+
console.error(err);
93+
$error = String(err);
94+
});
95+
}
9496
9597
onDestroy(() => {
9698
clearTimeout(errorToastTimeout);

src/routes/+layout.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ export const load = async ({ depends, fetch, url }) => {
2727
conv.title = conv.title.replace(/\p{Emoji}/gu, "");
2828
}
2929

30+
// Always strip <think> markers from titles for sidebar display
31+
conv.title = conv.title.replace(/<\/?think>/gi, "");
32+
3033
// remove invalid unicode and trim whitespaces
3134
conv.title = conv.title.replace(/\uFFFD/gu, "").trimStart();
3235

src/routes/conversation/+server.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,8 @@ export const POST: RequestHandler = async ({ locals, request }) => {
6363
error(404, "Conversation not found");
6464
}
6565

66-
title = conversation.title;
66+
// Strip <think> markers from imported titles
67+
title = conversation.title.replace(/<\/?think>/gi, "").trim();
6768
messages = conversation.messages;
6869
rootMessageId = conversation.rootMessageId ?? rootMessageId;
6970
values.model = conversation.model;
@@ -83,7 +84,8 @@ export const POST: RequestHandler = async ({ locals, request }) => {
8384

8485
const res = await collections.conversations.insertOne({
8586
_id: new ObjectId(),
86-
title: title || "New Chat",
87+
// Always store sanitized titles
88+
title: (title || "New Chat").replace(/<\/?think>/gi, "").trim(),
8789
rootMessageId,
8890
messages,
8991
model: values.model,

0 commit comments

Comments
 (0)