Skip to content

Commit 4de9619

Browse files
authored
🧠 fix: Handle Reasoning Chunk Edge Cases (#5800)
* refactor: better reasoning parsing * style: better model selector mobile styling * chore: bump vite
1 parent 404b27d commit 4de9619

File tree

9 files changed

+2897
-1982
lines changed

9 files changed

+2897
-1982
lines changed

api/app/clients/OpenAIClient.js

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -506,9 +506,8 @@ class OpenAIClient extends BaseClient {
506506
if (promptPrefix && this.isOmni === true) {
507507
const lastUserMessageIndex = payload.findLastIndex((message) => message.role === 'user');
508508
if (lastUserMessageIndex !== -1) {
509-
payload[
510-
lastUserMessageIndex
511-
].content = `${promptPrefix}\n${payload[lastUserMessageIndex].content}`;
509+
payload[lastUserMessageIndex].content =
510+
`${promptPrefix}\n${payload[lastUserMessageIndex].content}`;
512511
}
513512
}
514513

@@ -1072,10 +1071,24 @@ ${convo}
10721071
return '';
10731072
}
10741073

1075-
const reasoningTokens =
1076-
this.streamHandler.reasoningTokens.length > 0
1077-
? `:::thinking\n${this.streamHandler.reasoningTokens.join('')}\n:::\n`
1078-
: '';
1074+
let thinkMatch;
1075+
let remainingText;
1076+
let reasoningText = '';
1077+
1078+
if (this.streamHandler.reasoningTokens.length > 0) {
1079+
reasoningText = this.streamHandler.reasoningTokens.join('');
1080+
thinkMatch = reasoningText.match(/<think>([\s\S]*?)<\/think>/)?.[1]?.trim();
1081+
if (thinkMatch != null && thinkMatch) {
1082+
const reasoningTokens = `:::thinking\n${thinkMatch}\n:::\n`;
1083+
remainingText = reasoningText.split(/<\/think>/)?.[1]?.trim() || '';
1084+
return `${reasoningTokens}${remainingText}${this.streamHandler.tokens.join('')}`;
1085+
} else if (thinkMatch === '') {
1086+
remainingText = reasoningText.split(/<\/think>/)?.[1]?.trim() || '';
1087+
return `${remainingText}${this.streamHandler.tokens.join('')}`;
1088+
}
1089+
}
1090+
1091+
const reasoningTokens = reasoningText.length > 0 ? `:::thinking\n${reasoningText}\n:::\n` : '';
10791092

10801093
return `${reasoningTokens}${this.streamHandler.tokens.join('')}`;
10811094
}
@@ -1449,7 +1462,7 @@ ${convo}
14491462
this.options.context !== 'title' &&
14501463
message.content.startsWith('<think>')
14511464
) {
1452-
return message.content.replace('<think>', ':::thinking').replace('</think>', ':::');
1465+
return this.getStreamText();
14531466
}
14541467

14551468
return message.content;
@@ -1473,13 +1486,17 @@ ${convo}
14731486
(err instanceof OpenAI.OpenAIError && err?.message?.includes('missing finish_reason'))
14741487
) {
14751488
logger.error('[OpenAIClient] Known OpenAI error:', err);
1476-
if (intermediateReply.length > 0) {
1489+
if (this.streamHandler && this.streamHandler.reasoningTokens.length) {
1490+
return this.getStreamText();
1491+
} else if (intermediateReply.length > 0) {
14771492
return intermediateReply.join('');
14781493
} else {
14791494
throw err;
14801495
}
14811496
} else if (err instanceof OpenAI.APIError) {
1482-
if (intermediateReply.length > 0) {
1497+
if (this.streamHandler && this.streamHandler.reasoningTokens.length) {
1498+
return this.getStreamText();
1499+
} else if (intermediateReply.length > 0) {
14831500
return intermediateReply.join('');
14841501
} else {
14851502
throw err;

api/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
"@langchain/google-genai": "^0.1.7",
4646
"@langchain/google-vertexai": "^0.1.8",
4747
"@langchain/textsplitters": "^0.1.0",
48-
"@librechat/agents": "^2.0.3",
48+
"@librechat/agents": "^2.0.4",
4949
"@waylaidwanderer/fetch-event-source": "^3.0.1",
5050
"axios": "1.7.8",
5151
"bcryptjs": "^2.4.3",

client/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@
136136
"tailwindcss": "^3.4.1",
137137
"ts-jest": "^29.2.5",
138138
"typescript": "^5.3.3",
139-
"vite": "^5.4.14",
139+
"vite": "^6.1.0",
140140
"vite-plugin-node-polyfills": "^0.17.0",
141141
"vite-plugin-pwa": "^0.21.1"
142142
}

client/src/components/Chat/Messages/Content/ContentParts.tsx

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,24 @@ const ContentParts = memo(
5050
[attachments, messageAttachmentsMap, messageId],
5151
);
5252

53-
const hasReasoningParts = useMemo(
54-
() => content?.some((part) => part?.type === ContentTypes.THINK && part.think) ?? false,
55-
[content],
56-
);
53+
const hasReasoningParts = useMemo(() => {
54+
const hasThinkPart = content?.some((part) => part?.type === ContentTypes.THINK) ?? false;
55+
const allThinkPartsHaveContent =
56+
content?.every((part) => {
57+
if (part?.type !== ContentTypes.THINK) {
58+
return true;
59+
}
60+
61+
if (typeof part.think === 'string') {
62+
const cleanedContent = part.think.replace(/<\/?think>/g, '').trim();
63+
return cleanedContent.length > 0;
64+
}
65+
66+
return false;
67+
}) ?? false;
5768

69+
return hasThinkPart && allThinkPartsHaveContent;
70+
}, [content]);
5871
if (!content) {
5972
return null;
6073
}

client/src/components/Chat/Messages/Content/MessageContent.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,9 @@ const MessageContent = ({
159159

160160
return (
161161
<>
162-
{thinkingContent && <Thinking key={`thinking-${messageId}`}>{thinkingContent}</Thinking>}
162+
{thinkingContent.length > 0 && (
163+
<Thinking key={`thinking-${messageId}`}>{thinkingContent}</Thinking>
164+
)}
163165
<DisplayMessage
164166
key={`display-${messageId}`}
165167
showCursor={showRegularCursor}

client/src/components/Chat/Messages/Content/Parts/Reasoning.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,16 @@ type ReasoningProps = {
1111
const Reasoning = memo(({ reasoning }: ReasoningProps) => {
1212
const { isExpanded, nextType } = useMessageContext();
1313
const reasoningText = useMemo(() => {
14-
return reasoning.replace(/^<think>\s*/, '').replace(/\s*<\/think>$/, '');
14+
return reasoning
15+
.replace(/^<think>\s*/, '')
16+
.replace(/\s*<\/think>$/, '')
17+
.trim();
1518
}, [reasoning]);
1619

20+
if (!reasoningText) {
21+
return null;
22+
}
23+
1724
return (
1825
<div
1926
className={cn(

client/src/components/Input/ModelSelect/TemporaryChat.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,15 @@ export const TemporaryChat = () => {
4040
};
4141

4242
return (
43-
<div className="sticky bottom-0 border-none bg-surface-tertiary px-6 py-4 ">
44-
<div className="flex items-center">
45-
<div className={cn('flex flex-1 items-center gap-2', isActiveConvo && 'opacity-40')}>
43+
<div className="sticky bottom-0 mt-auto w-full border-none bg-surface-tertiary px-6 py-4">
44+
<div className="flex items-center justify-between">
45+
<div className={cn('flex items-center gap-2', isActiveConvo && 'opacity-40')}>
4646
<MessageCircleDashed className="icon-sm" aria-hidden="true" />
47-
<span className="text-sm text-text-primary">{localize('com_ui_temporary_chat')}</span>
47+
<span className="truncate text-sm text-text-primary">
48+
{localize('com_ui_temporary_chat')}
49+
</span>
4850
</div>
49-
<div className="ml-auto flex items-center">
51+
<div className="flex flex-shrink-0 items-center">
5052
<Switch
5153
id="temporary-chat-switch"
5254
checked={isTemporary}

client/src/components/ui/SelectDropDownPop.tsx

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -56,31 +56,32 @@ function SelectDropDownPop({
5656

5757
return (
5858
<Root>
59-
<div className={'flex items-center justify-center gap-2 '}>
59+
<div className={'flex items-center justify-center gap-2'}>
6060
<div className={'relative w-full'}>
6161
<Trigger asChild>
6262
<button
6363
data-testid="select-dropdown-button"
6464
className={cn(
6565
'pointer-cursor relative flex flex-col rounded-lg border border-black/10 bg-white py-2 pl-3 pr-10 text-left focus:ring-0 focus:ring-offset-0 dark:border-gray-700 dark:bg-gray-800 sm:text-sm',
6666
'hover:bg-gray-50 radix-state-open:bg-gray-50 dark:hover:bg-gray-700 dark:radix-state-open:bg-gray-700',
67+
'min-w-[200px] max-w-[215px] sm:min-w-full sm:max-w-full',
6768
)}
6869
aria-label={`Select ${title}`}
6970
aria-haspopup="false"
7071
>
7172
{' '}
7273
{showLabel && (
73-
<label className="block text-xs text-gray-700 dark:text-gray-500 ">{title}</label>
74+
<label className="block text-xs text-gray-700 dark:text-gray-500">{title}</label>
7475
)}
75-
<span className="inline-flex w-full ">
76+
<span className="inline-flex w-full">
7677
<span
7778
className={cn(
78-
'flex h-6 items-center gap-1 text-sm text-gray-800 dark:text-white',
79+
'flex h-6 items-center gap-1 text-sm text-text-primary',
7980
!showLabel ? 'text-xs' : '',
8081
'min-w-[75px] font-normal',
8182
)}
8283
>
83-
{typeof value !== 'string' && value ? value.label ?? '' : value ?? ''}
84+
{typeof value !== 'string' && value ? (value.label ?? '') : (value ?? '')}
8485
</span>
8586
</span>
8687
<span className="absolute inset-y-0 right-0 flex items-center pr-2">
@@ -91,7 +92,7 @@ function SelectDropDownPop({
9192
viewBox="0 0 24 24"
9293
strokeLinecap="round"
9394
strokeLinejoin="round"
94-
className="h-4 w-4 text-gray-400"
95+
className="h-4 w-4 text-gray-400"
9596
height="1em"
9697
width="1em"
9798
xmlns="http://www.w3.org/2000/svg"
@@ -107,7 +108,7 @@ function SelectDropDownPop({
107108
side="bottom"
108109
align="start"
109110
className={cn(
110-
'mt-2 max-h-[52vh] min-w-full overflow-hidden overflow-y-auto rounded-lg border border-gray-200 bg-white shadow-lg dark:border-gray-700 dark:bg-gray-700 dark:text-white lg:max-h-[52vh]',
111+
'mr-3 mt-2 max-h-[52vh] w-full max-w-[85vw] overflow-hidden overflow-y-auto rounded-lg border border-gray-200 bg-white shadow-lg dark:border-gray-700 dark:bg-gray-700 dark:text-white sm:max-w-full lg:max-h-[52vh]',
111112
hasSearchRender && 'relative',
112113
)}
113114
>

0 commit comments

Comments
 (0)