trim whitespace + fix onFinalMessage last chunk error

This commit is contained in:
Andrew Pareles 2025-04-08 02:44:05 -07:00
parent 4e197f4659
commit e331fb4eed
3 changed files with 51 additions and 18 deletions

View file

@ -721,6 +721,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
onFinalMessage: async ({ fullText, fullReasoning, toolCall, anthropicReasoning, }) => {
this._addMessageToThread(threadId, { role: 'assistant', content: fullText, reasoning: fullReasoning, anthropicReasoning })
this._setStreamState(threadId, { messageSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined, toolCallSoFar: undefined }, 'merge')
console.log('tool call!!', toolCall)
resMessageIsDonePromise(toolCall) // resolve with tool calls
},
onError: (error) => {

View file

@ -13,7 +13,7 @@ import { createSaxParser } from './sax.js'
// =============== reasoning ===============
// could simplify this - this assumes we can never add a tag without committing it to the user's screen, but that's not true
export const extractReasoningOnTextWrapper = (
export const extractReasoningWrapper = (
onText: OnText, onFinalMessage: OnFinalMessage, thinkTags: [string, string]
): { newOnText: OnText, newOnFinalMessage: OnFinalMessage } => {
let latestAddIdx = 0 // exclusive index in fullText_
@ -122,6 +122,10 @@ export const extractReasoningOnTextWrapper = (
}
const newOnFinalMessage: OnFinalMessage = (params) => {
// treat like just got text before calling onFinalMessage (or else we sometimes miss the final chunk that's new to finalMessage)
newOnText({ ...params })
const { fullText, fullReasoning } = getOnFinalMessageParams()
onFinalMessage({ ...params, fullText, fullReasoning })
}
@ -145,7 +149,7 @@ type ToolsState = {
currentToolCall: RawToolCallObj,
}
export const extractToolsOnTextWrapper = (
export const extractToolsWrapper = (
onText: OnText, onFinalMessage: OnFinalMessage, chatMode: ChatMode
): { newOnText: OnText, newOnFinalMessage: OnFinalMessage } => {
const tools = availableTools(chatMode)
@ -288,16 +292,44 @@ export const extractToolsOnTextWrapper = (
const newOnFinalMessage: OnFinalMessage = (params) => {
// treat like just got text before calling onFinalMessage (or else we sometimes miss the final chunk that's new to finalMessage)
newOnText({ ...params })
console.log('final message!!!', trueFullText)
console.log('----- returning ----\n', fullText)
console.log('----- tools ----\n', JSON.stringify(currentToolCalls, null, 2))
onFinalMessage({ ...params, fullText, toolCall: currentToolCalls[0] })
}
fullText = fullText.trimEnd()
const toolCall = currentToolCalls[0]
if (toolCall) {
// trim off all whitespace at and before first \n and after last \n for each param
for (const paramName in toolCall.rawParams) {
const orig = toolCall.rawParams[paramName]
if (orig === undefined) continue
toolCall.rawParams[paramName] = trimBeforeAndAfterNewLines(orig)
}
}
onFinalMessage({ ...params, fullText, toolCall: currentToolCalls.length > 0 ? currentToolCalls[0] : undefined })
}
return { newOnText, newOnFinalMessage };
}
// trim all whitespace up until the first newline, and all whitespace after the last newline
const trimBeforeAndAfterNewLines = (s: string) => {
if (!s) return s;
const firstNewLineIndex = s.indexOf('\n');
if (firstNewLineIndex !== -1 && s.substring(0, firstNewLineIndex).trim() === '') {
s = s.substring(firstNewLineIndex + 1, Infinity)
}
const lastNewLineIndex = s.lastIndexOf('\n');
if (lastNewLineIndex !== -1 && s.substring(lastNewLineIndex + 1, Infinity).trim() === '') {
s = s.substring(0, lastNewLineIndex)
}
return s
}

View file

@ -11,7 +11,7 @@ import { LLMChatMessage, LLMFIMMessage, ModelListParams, OllamaModelResponse, On
import { ChatMode, defaultProviderSettings, displayInfoOfProviderName, ModelSelectionOptions, ProviderName, SettingsOfProvider } from '../../common/voidSettingsTypes.js';
import { prepareFIMMessage, prepareMessages } from './preprocessLLMMessages.js';
import { getSendableReasoningInfo, getModelCapabilities, getProviderCapabilities } from '../../common/modelCapabilities.js';
import { extractReasoningOnTextWrapper, extractToolsOnTextWrapper } from './extractGrammar.js';
import { extractReasoningWrapper, extractToolsWrapper } from './extractGrammar.js';
type InternalCommonMessageParams = {
@ -156,14 +156,14 @@ const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage
const { needsManualParse: needsManualReasoningParse, nameOfFieldInDelta: nameOfReasoningFieldInDelta } = providerReasoningIOSettings?.output ?? {}
const manuallyParseReasoning = needsManualReasoningParse && canIOReasoning && openSourceThinkTags
if (manuallyParseReasoning) {
const { newOnText, newOnFinalMessage } = extractReasoningOnTextWrapper(onText, onFinalMessage, openSourceThinkTags)
const { newOnText, newOnFinalMessage } = extractReasoningWrapper(onText, onFinalMessage, openSourceThinkTags)
onText = newOnText
onFinalMessage = newOnFinalMessage
}
// manually parse out tool results
if (chatMode) {
const { newOnText, newOnFinalMessage } = extractToolsOnTextWrapper(onText, onFinalMessage, chatMode)
const { newOnText, newOnFinalMessage } = extractToolsWrapper(onText, onFinalMessage, chatMode)
onText = newOnText
onFinalMessage = newOnFinalMessage
}
@ -281,7 +281,7 @@ const sendAnthropicChat = ({ messages: messages_, providerName, onText, onFinalM
// manually parse out tool results
if (chatMode) {
const { newOnText, newOnFinalMessage } = extractToolsOnTextWrapper(onText, onFinalMessage, chatMode)
const { newOnText, newOnFinalMessage } = extractToolsWrapper(onText, onFinalMessage, chatMode)
onText = newOnText
onFinalMessage = newOnFinalMessage
}
@ -290,8 +290,8 @@ const sendAnthropicChat = ({ messages: messages_, providerName, onText, onFinalM
let fullText = ''
let fullReasoning = ''
let fullToolName = ''
let fullToolParams = ''
// let fullToolName = ''
// let fullToolParams = ''
// there are no events for tool_use, it comes in at the end
stream.on('streamEvent', e => {
@ -313,10 +313,10 @@ const sendAnthropicChat = ({ messages: messages_, providerName, onText, onFinalM
fullReasoning += '[redacted_thinking]'
onText({ fullText, fullReasoning, })
}
else if (e.content_block.type === 'tool_use') {
fullToolName += e.content_block.name ?? '' // anthropic gives us the tool name in the start block
onText({ fullText, fullReasoning, })
}
// else if (e.content_block.type === 'tool_use') {
// fullToolName += e.content_block.name ?? '' // anthropic gives us the tool name in the start block
// onText({ fullText, fullReasoning, })
// }
}
// delta
@ -329,10 +329,10 @@ const sendAnthropicChat = ({ messages: messages_, providerName, onText, onFinalM
fullReasoning += e.delta.thinking
onText({ fullText, fullReasoning, })
}
else if (e.delta.type === 'input_json_delta') { // tool use
fullToolParams += e.delta.partial_json ?? '' // anthropic gives us the partial delta (string) here - https://docs.anthropic.com/en/api/messages-streaming
onText({ fullText, fullReasoning, })
}
// else if (e.delta.type === 'input_json_delta') { // tool use
// fullToolParams += e.delta.partial_json ?? '' // anthropic gives us the partial delta (string) here - https://docs.anthropic.com/en/api/messages-streaming
// onText({ fullText, fullReasoning, })
// }
}
})