Skip to content

Commit 953118a

Browse files
committed
fix: improve Mimo thinking mode parsing and refactor forwarder tool calls handling
1 parent e2633eb commit 953118a

7 files changed

Lines changed: 108 additions & 146 deletions

File tree

src/main/proxy/adapters/mimo.ts

Lines changed: 65 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -202,8 +202,16 @@ function stripCitationsWithBuffer(text: string, buffer: { value: string }): stri
202202

203203
function stripThinkTags(text: string): string {
204204
text = text.replace(/\u0000/g, '')
205+
// Remove opening think tag (including partial ones at the beginning)
205206
text = text.replace(/^<think[^>]*>/, '')
207+
// Remove &gt; entity which might be part of a broken tag
206208
text = text.replace(/^&gt;/, '')
209+
// Also handle cases where only part of the tag is present
210+
text = text.replace(/^hink>/, '')
211+
text = text.replace(/^ink>/, '')
212+
text = text.replace(/^nk>/, '')
213+
text = text.replace(/^k>/, '')
214+
text = text.replace(/^>/, '')
207215
return text
208216
}
209217

@@ -385,9 +393,14 @@ export class MimoStreamHandler {
385393

386394
let buffer = ''
387395
let currentEvent = ''
396+
397+
// Track state and content
388398
let state: 'init' | 'thinking' | 'content' = 'init'
389-
let thinkContent = ''
390-
let normalContent = ''
399+
let totalContent = ''
400+
let lastProcessedIndex = 0
401+
let thinkEndTagFound = false
402+
const thinkEndTag1 = '</think>'
403+
const thinkEndTag2 = '</thinkgt;'
391404

392405
for await (const chunk of stream) {
393406
buffer += chunk.toString()
@@ -406,57 +419,72 @@ export class MimoStreamHandler {
406419
const mimoChunk: MimoChunk = { type: currentEvent as any, ...data }
407420

408421
if ((mimoChunk.type === 'message' || mimoChunk.type === 'text') && mimoChunk.content) {
409-
const text = (mimoChunk.content ?? '').replace(/\u0000/g, '')
410-
this.content += text
422+
const newText = (mimoChunk.content ?? '').replace(/\u0000/g, '')
423+
totalContent += newText
411424

412425
if (state === 'init') {
413-
const thinkStartIdx = this.content.indexOf('<think')
426+
const thinkStartIdx = totalContent.indexOf('<think')
414427
if (thinkStartIdx !== -1) {
415428
state = 'thinking'
429+
// Skip any content before <think tag
430+
lastProcessedIndex = thinkStartIdx
416431
} else {
417432
state = 'content'
418433
}
419434
}
420435

421436
if (state === 'thinking') {
422-
const thinkEndIdx = this.content.indexOf('</think>')
423-
const thinkEndIdx2 = this.content.indexOf('</thinkgt;')
424-
const actualThinkEndIdx = thinkEndIdx !== -1 ? thinkEndIdx : thinkEndIdx2
425-
426-
if (actualThinkEndIdx !== -1) {
427-
const thinkPart = this.content.slice(0, actualThinkEndIdx)
428-
const newThink = stripThinkTags(thinkPart)
429-
const deltaThink = newThink.slice(thinkContent.length)
430-
if (deltaThink && this.thinkingMode === 'separate') {
431-
yield this.formatOpenAIChunk(id, created, { reasoning_content: stripCitationsWithBuffer(deltaThink, this.thinkingCitationBuffer) })
437+
if (!thinkEndTagFound) {
438+
// Look for think end tag
439+
let thinkEndIdx = totalContent.indexOf(thinkEndTag1, lastProcessedIndex)
440+
let actualEndTag = thinkEndTag1
441+
442+
if (thinkEndIdx === -1) {
443+
thinkEndIdx = totalContent.indexOf(thinkEndTag2, lastProcessedIndex)
444+
actualEndTag = thinkEndTag2
432445
}
433-
thinkContent = newThink
434446

435-
state = 'content'
436-
const rest = this.content.slice(actualThinkEndIdx + (thinkEndIdx !== -1 ? 8 : 10))
437-
if (rest.trim()) {
438-
const newNormal = stripCitationsWithBuffer(rest, this.citationBuffer)
439-
const deltaNormal = newNormal.slice(normalContent.length)
440-
if (deltaNormal) {
441-
yield this.formatOpenAIChunk(id, created, { content: deltaNormal })
447+
if (thinkEndIdx !== -1) {
448+
// Found the end of thinking
449+
thinkEndTagFound = true
450+
451+
// Extract the thinking content between lastProcessedIndex and thinkEndIdx
452+
const thinkContent = totalContent.slice(lastProcessedIndex, thinkEndIdx)
453+
const cleanedThink = stripThinkTags(thinkContent)
454+
const cleanedThinkWithCitations = stripCitationsWithBuffer(cleanedThink, this.thinkingCitationBuffer)
455+
456+
if (cleanedThinkWithCitations && this.thinkingMode === 'separate') {
457+
yield this.formatOpenAIChunk(id, created, { reasoning_content: cleanedThinkWithCitations })
442458
}
443-
normalContent = newNormal
444-
}
445-
} else {
446-
const newThink = stripThinkTags(this.content)
447-
const deltaThink = newThink.slice(thinkContent.length)
448-
if (deltaThink && this.thinkingMode === 'separate') {
449-
yield this.formatOpenAIChunk(id, created, { reasoning_content: stripCitationsWithBuffer(deltaThink, this.thinkingCitationBuffer) })
459+
460+
// Move past the end tag
461+
lastProcessedIndex = thinkEndIdx + actualEndTag.length
462+
state = 'content'
463+
} else {
464+
// Still in thinking, process new content
465+
const thinkContent = totalContent.slice(lastProcessedIndex)
466+
const cleanedThink = stripThinkTags(thinkContent)
467+
const cleanedThinkWithCitations = stripCitationsWithBuffer(cleanedThink, this.thinkingCitationBuffer)
468+
469+
if (cleanedThinkWithCitations && this.thinkingMode === 'separate') {
470+
yield this.formatOpenAIChunk(id, created, { reasoning_content: cleanedThinkWithCitations })
471+
}
472+
473+
lastProcessedIndex = totalContent.length
450474
}
451-
thinkContent = newThink
452475
}
453-
} else if (state === 'content') {
454-
const newNormal = stripCitationsWithBuffer(this.content, this.citationBuffer)
455-
const deltaNormal = newNormal.slice(normalContent.length)
456-
if (deltaNormal) {
457-
yield this.formatOpenAIChunk(id, created, { content: deltaNormal })
476+
}
477+
478+
if (state === 'content' && lastProcessedIndex < totalContent.length) {
479+
// Process content after thinking
480+
const contentPart = totalContent.slice(lastProcessedIndex)
481+
const cleanedContent = stripCitationsWithBuffer(contentPart, this.citationBuffer)
482+
483+
if (cleanedContent) {
484+
yield this.formatOpenAIChunk(id, created, { content: cleanedContent })
458485
}
459-
normalContent = newNormal
486+
487+
lastProcessedIndex = totalContent.length
460488
}
461489
} else if (mimoChunk.type === 'usage' && mimoChunk.usage) {
462490
this.usage = mimoChunk.usage

src/main/proxy/forwarder.ts

Lines changed: 25 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -90,10 +90,23 @@ export class RequestForwarder {
9090
return null
9191
}
9292

93-
/**
94-
* Check if messages contain MCP-style tool definitions
95-
* MCP tools are defined in system message using <tools><tool> XML format
96-
*/
93+
private applyToolCallsToResponse(
94+
result: any,
95+
model: string,
96+
tools: any[] | undefined
97+
): void {
98+
if (tools && tools.length > 0 && !isNativeFunctionCallingModel(model)) {
99+
const content = result?.choices?.[0]?.message?.content || ''
100+
const toolCalls = this.parseToolCallsFromContent(content)
101+
102+
if (toolCalls && toolCalls.length > 0) {
103+
result.choices[0].message.tool_calls = toolCalls
104+
result.choices[0].message.content = null
105+
result.choices[0].finish_reason = 'tool_calls'
106+
}
107+
}
108+
}
109+
97110
private hasMCPToolDefinitions(messages: any[]): boolean {
98111
for (const msg of messages) {
99112
if (msg.role === 'system' && typeof msg.content === 'string') {
@@ -636,20 +649,8 @@ CRITICAL RULES:
636649
// Non-streaming requests need to collect stream data and convert
637650
const result = await handler.handleNonStream(response.data)
638651

639-
// Parse tool calls from response content if using prompt-based tool calling
640-
if (request.tools && request.tools.length > 0 && !isNativeFunctionCallingModel(request.model)) {
641-
const content = result?.choices?.[0]?.message?.content || ''
642-
const toolCalls = this.parseToolCallsFromContent(content)
643-
644-
if (toolCalls && toolCalls.length > 0) {
645-
// Found tool calls in response, add them to the response
646-
result.choices[0].message.tool_calls = toolCalls
647-
result.choices[0].message.content = null
648-
result.choices[0].finish_reason = 'tool_calls'
649-
}
650-
}
652+
this.applyToolCallsToResponse(result, request.model, request.tools)
651653

652-
// Delete session after non-streaming request ends
653654
if (deleteSessionCallback) {
654655
await deleteSessionCallback()
655656
}
@@ -757,19 +758,8 @@ CRITICAL RULES:
757758

758759
const result = await handler.handleNonStream(response.data)
759760

760-
// Parse tool calls from response content if using prompt-based tool calling
761-
if (request.tools && request.tools.length > 0 && !isNativeFunctionCallingModel(request.model)) {
762-
const content = result?.choices?.[0]?.message?.content || ''
763-
const toolCalls = this.parseToolCallsFromContent(content)
764-
765-
if (toolCalls && toolCalls.length > 0) {
766-
result.choices[0].message.tool_calls = toolCalls
767-
result.choices[0].message.content = null
768-
result.choices[0].finish_reason = 'tool_calls'
769-
}
770-
}
761+
this.applyToolCallsToResponse(result, request.model, request.tools)
771762

772-
// Delete session after non-stream response
773763
if (shouldDeleteSession()) {
774764
const convId = handler.getConversationId()
775765
if (convId) {
@@ -860,19 +850,8 @@ CRITICAL RULES:
860850

861851
const result = await handler.handleNonStream(response.data)
862852

863-
// Parse tool calls from response content if using prompt-based tool calling
864-
if (request.tools && request.tools.length > 0 && !isNativeFunctionCallingModel(request.model)) {
865-
const content = result?.choices?.[0]?.message?.content || ''
866-
const toolCalls = this.parseToolCallsFromContent(content)
867-
868-
if (toolCalls && toolCalls.length > 0) {
869-
result.choices[0].message.tool_calls = toolCalls
870-
result.choices[0].message.content = null
871-
result.choices[0].finish_reason = 'tool_calls'
872-
}
873-
}
853+
this.applyToolCallsToResponse(result, request.model, request.tools)
874854

875-
// Delete conversation if needed
876855
if (shouldDeleteSession()) {
877856
const realChatId = handler.getConversationId()
878857
if (realChatId && realChatId.startsWith('kimi-') === false) {
@@ -967,17 +946,7 @@ CRITICAL RULES:
967946

968947
const result = await handler.handleNonStream(response.data, response)
969948

970-
// Parse tool calls from response content if using prompt-based tool calling
971-
if (request.tools && request.tools.length > 0 && !isNativeFunctionCallingModel(request.model)) {
972-
const content = result?.choices?.[0]?.message?.content || ''
973-
const toolCalls = this.parseToolCallsFromContent(content)
974-
975-
if (toolCalls && toolCalls.length > 0) {
976-
result.choices[0].message.tool_calls = toolCalls
977-
result.choices[0].message.content = null
978-
result.choices[0].finish_reason = 'tool_calls'
979-
}
980-
}
949+
this.applyToolCallsToResponse(result, request.model, request.tools)
981950

982951
const sid = handler.getSessionId()
983952
if (deleteSessionCallback && sid) {
@@ -1066,17 +1035,7 @@ CRITICAL RULES:
10661035

10671036
const result = await handler.handleNonStream(response.data)
10681037

1069-
// Parse tool calls from response content if using prompt-based tool calling
1070-
if (request.tools && request.tools.length > 0 && !isNativeFunctionCallingModel(request.model)) {
1071-
const content = result?.choices?.[0]?.message?.content || ''
1072-
const toolCalls = this.parseToolCallsFromContent(content)
1073-
1074-
if (toolCalls && toolCalls.length > 0) {
1075-
result.choices[0].message.tool_calls = toolCalls
1076-
result.choices[0].message.content = null
1077-
result.choices[0].finish_reason = 'tool_calls'
1078-
}
1079-
}
1038+
this.applyToolCallsToResponse(result, request.model, request.tools)
10801039

10811040
if (deleteChatCallback) {
10821041
await deleteChatCallback(chatId)
@@ -1167,17 +1126,7 @@ CRITICAL RULES:
11671126

11681127
const result = await handler.handleNonStream(response.data)
11691128

1170-
// Parse tool calls from response content if using prompt-based tool calling
1171-
if (request.tools && request.tools.length > 0 && !isNativeFunctionCallingModel(request.model)) {
1172-
const content = result?.choices?.[0]?.message?.content || ''
1173-
const toolCalls = this.parseToolCallsFromContent(content)
1174-
1175-
if (toolCalls && toolCalls.length > 0) {
1176-
result.choices[0].message.tool_calls = toolCalls
1177-
result.choices[0].message.content = null
1178-
result.choices[0].finish_reason = 'tool_calls'
1179-
}
1180-
}
1129+
this.applyToolCallsToResponse(result, request.model, request.tools)
11811130

11821131
if (deleteChatCallback) {
11831132
await deleteChatCallback(chatId)
@@ -1273,19 +1222,8 @@ CRITICAL RULES:
12731222
}
12741223

12751224
if (response) {
1276-
// Parse tool calls from response content if using prompt-based tool calling
1277-
if (request.tools && request.tools.length > 0 && !isNativeFunctionCallingModel(request.model)) {
1278-
const content = response.data?.choices?.[0]?.message?.content || ''
1279-
const toolCalls = this.parseToolCallsFromContent(content)
1280-
1281-
if (toolCalls && toolCalls.length > 0) {
1282-
response.data.choices[0].message.tool_calls = toolCalls
1283-
response.data.choices[0].message.content = null
1284-
response.data.choices[0].finish_reason = 'tool_calls'
1285-
}
1286-
}
1225+
this.applyToolCallsToResponse(response.data, request.model, request.tools)
12871226

1288-
// Response is already formatted as OpenAI-compatible format
12891227
if (deleteChatCallback) {
12901228
await deleteChatCallback(chatId)
12911229
}
@@ -1454,19 +1392,8 @@ CRITICAL RULES:
14541392
const handler = new PerplexityStreamHandler(actualModel, sessionId, undefined, adapter)
14551393
const result = await handler.handleNonStream(stream)
14561394

1457-
// Parse tool calls from response content if tools were provided
1458-
if (request.tools && request.tools.length > 0 && !isNativeFunctionCallingModel(request.model)) {
1459-
const content = result?.choices?.[0]?.message?.content || ''
1460-
const toolCalls = this.parseToolCallsFromContent(content)
1461-
1462-
if (toolCalls && toolCalls.length > 0) {
1463-
result.choices[0].message.tool_calls = toolCalls
1464-
result.choices[0].message.content = null
1465-
result.choices[0].finish_reason = 'tool_calls'
1466-
}
1467-
}
1395+
this.applyToolCallsToResponse(result, request.model, request.tools)
14681396

1469-
// Delete session after non-stream response
14701397
if (shouldDeleteSession()) {
14711398
await adapter.deleteSession(sessionId)
14721399
}

src/renderer/src/components/logs/RequestLogList.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useState, useEffect, useRef, useCallback, useMemo, type ReactElement } from 'react'
22
import { useTranslation } from 'react-i18next'
3-
import { List } from 'react-window'
3+
import { List, type RowComponentProps } from 'react-window'
44
import { Badge } from '@/components/ui/badge'
55
import { Button } from '@/components/ui/button'
66
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
@@ -165,15 +165,15 @@ export function RequestLogList() {
165165
}, [])
166166

167167
const RowComponent = useCallback(
168-
({ index, style }: { index: number; style: React.CSSProperties }): ReactElement | null => {
169-
const log = logs[index]
168+
({ index, style, logs: rowLogs, onSelectLog }: RowComponentProps<RowProps>): ReactElement | null => {
169+
const log = rowLogs[index]
170170
if (!log) return null
171171

172172
return (
173173
<div
174174
style={style}
175175
className="px-2 pb-2"
176-
onClick={() => handleSelectLog(log)}
176+
onClick={() => onSelectLog(log)}
177177
>
178178
<div className="flex items-center gap-3 p-3 rounded-lg bg-muted/30 hover:bg-muted/50 cursor-pointer transition-colors h-[68px]">
179179
<Badge variant="outline" className={getStatusColor(log.status, log.statusCode)}>
@@ -206,7 +206,7 @@ export function RequestLogList() {
206206
</div>
207207
)
208208
},
209-
[logs, handleSelectLog]
209+
[]
210210
)
211211

212212
const rowProps = useMemo<RowProps>(() => ({

src/renderer/src/components/providers/AddAccountDialog.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,13 +90,21 @@ function mapOAuthCredentials(providerId: string | undefined, credentials: Record
9090
// For Mimo, map all three tokens
9191
if (providerId === 'mimo') {
9292
const result: Record<string, string> = {}
93-
if (credentials['serviceToken']) {
93+
// OAuth already returns credentials in correct format (service_token, user_id, ph_token)
94+
// Check for final format first
95+
if (credentials['service_token']) {
96+
result['service_token'] = credentials['service_token']
97+
} else if (credentials['serviceToken']) {
9498
result['service_token'] = credentials['serviceToken']
9599
}
96-
if (credentials['userId']) {
100+
if (credentials['user_id']) {
101+
result['user_id'] = credentials['user_id']
102+
} else if (credentials['userId']) {
97103
result['user_id'] = credentials['userId']
98104
}
99-
if (credentials['xiaomichatbot_ph']) {
105+
if (credentials['ph_token']) {
106+
result['ph_token'] = credentials['ph_token']
107+
} else if (credentials['xiaomichatbot_ph']) {
100108
result['ph_token'] = credentials['xiaomichatbot_ph']
101109
}
102110
return result

0 commit comments

Comments
 (0)