From 7558d4dc1c50f295333133bee342fff903b4afa4 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Fri, 7 Mar 2025 20:28:23 -0800 Subject: [PATCH] anthropic reasoning works with tools --- .../contrib/void/browser/chatThreadService.ts | 27 ++++++------ .../contrib/void/browser/editCodeService.ts | 2 +- .../react/src/sidebar-tsx/SidebarChat.tsx | 1 + .../contrib/void/browser/toolsService.ts | 2 +- .../contrib/void/common/llmMessageTypes.ts | 5 ++- .../llmMessage/preprocessLLMMessages.ts | 44 +++++++++++++++---- .../llmMessage/sendLLMMessage.impl.ts | 15 ++++--- 7 files changed, 62 insertions(+), 34 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index f95fd790..8dcd8f72 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -14,7 +14,7 @@ import { IRange } from '../../../../editor/common/core/range.js'; import { ILLMMessageService } from '../common/llmMessageService.js'; import { chat_userMessageContent, chat_systemMessage, chat_lastUserMessageWithFilesAdded, chat_selectionsString } from './prompt/prompts.js'; import { InternalToolInfo, IToolsService, ToolCallParams, ToolResultType, ToolName, toolNamesThatRequireApproval, voidTools } from './toolsService.js'; -import { LLMChatMessage, ToolCallType } from '../common/llmMessageTypes.js'; +import { AnthropicReasoning, LLMChatMessage, ToolCallType } from '../common/llmMessageTypes.js'; import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; import { IVoidFileService } from '../common/voidFileService.js'; import { generateUuid } from '../../../../base/common/uuid.js'; @@ -40,7 +40,7 @@ const toLLMChatMessages = (chatMessages: ChatMessage[]): LLMChatMessage[] => { llmChatMessages.push({ role: c.role, content: c.content }) } else if (c.role === 'assistant') - llmChatMessages.push({ role: c.role, content: c.content }) + llmChatMessages.push({ role: c.role, content: c.content, anthropicReasoning: c.anthropicReasoning }) else if (c.role === 'tool') llmChatMessages.push({ role: c.role, id: c.id, name: c.name, params: c.paramsStr, content: c.content }) else if (c.role === 'tool_request') { @@ -108,6 +108,8 @@ export type ChatMessage = role: 'assistant'; content: string; // content received from LLM - allowed to be '', will be replaced with (empty) reasoning: string; // reasoning from the LLM, used for step-by-step thinking + + anthropicReasoning: AnthropicReasoning[] | null; // anthropic reasoning } | ToolMessage | ToolRequestApproval @@ -312,13 +314,6 @@ class ChatThreadService extends Disposable implements IChatThreadService { // ---------- streaming ---------- - private _finishStreamingTextMessage = (threadId: string, options: { content: string, reasoning: string }, error?: { message: string, fullError: Error | null }) => { - // add assistant's message to chat history, and clear selection - this._addMessageToThread(threadId, { role: 'assistant', content: options.content, reasoning: options.reasoning }) - this._setStreamState(threadId, { messageSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined, error }) - } - - async editUserMessageAndStreamResponse({ userMessage, chatMode, messageIdx }: { userMessage: string, chatMode: ChatMode, messageIdx: number }) { @@ -429,13 +424,14 @@ class ChatThreadService extends Disposable implements IChatThreadService { onText: ({ fullText, fullReasoning }) => { this._setStreamState(threadId, { messageSoFar: fullText, reasoningSoFar: fullReasoning }) }, - onFinalMessage: async ({ fullText, toolCalls, fullReasoning }) => { + onFinalMessage: async ({ fullText, toolCalls, fullReasoning, anthropicReasoning }) => { if ((toolCalls?.length ?? 0) === 0) { - this._finishStreamingTextMessage(threadId, { content: fullText, reasoning: fullReasoning }) + this._addMessageToThread(threadId, { role: 'assistant', content: fullText, reasoning: fullReasoning, anthropicReasoning }) + this._setStreamState(threadId, { messageSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined }) } else { - this._addMessageToThread(threadId, { role: 'assistant', content: fullText, reasoning: fullReasoning }) + this._addMessageToThread(threadId, { role: 'assistant', content: fullText, reasoning: fullReasoning, anthropicReasoning }) this._setStreamState(threadId, { messageSoFar: undefined, reasoningSoFar: undefined }) // clear streaming message // deal with the tool @@ -530,7 +526,9 @@ class ChatThreadService extends Disposable implements IChatThreadService { onError: (error) => { const messageSoFar = this.streamState[threadId]?.messageSoFar ?? '' const reasoningSoFar = this.streamState[threadId]?.reasoningSoFar ?? '' - this._finishStreamingTextMessage(threadId, { content: messageSoFar, reasoning: reasoningSoFar }, error) + // add assistant's message to chat history, and clear selection + this._addMessageToThread(threadId, { role: 'assistant', content: messageSoFar, reasoning: reasoningSoFar, anthropicReasoning: null }) + this._setStreamState(threadId, { messageSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined, error }) res_() }, }) @@ -552,7 +550,8 @@ class ChatThreadService extends Disposable implements IChatThreadService { if (llmCancelToken !== undefined) this._llmMessageService.abort(llmCancelToken) const messageSoFar = this.streamState[threadId]?.messageSoFar ?? '' const reasoningSoFar = this.streamState[threadId]?.reasoningSoFar ?? '' - this._finishStreamingTextMessage(threadId, { content: messageSoFar, reasoning: reasoningSoFar }) + this._addMessageToThread(threadId, { role: 'assistant', content: messageSoFar, reasoning: reasoningSoFar, anthropicReasoning: null }) + this._setStreamState(threadId, { messageSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined }) } dismissStreamError(threadId: string): void { diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index 2424f218..d67d84b4 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -1613,7 +1613,7 @@ class EditCodeService extends Disposable implements IEditCodeService { if (typeof originalBounds === 'string') { const content = errMsgOfInvalidStr(originalBounds, block.orig) messages.push( - { role: 'assistant', content: fullText }, // latest output + { role: 'assistant', content: fullText, anthropicReasoning: null }, // latest output { role: 'user', content: content } // user explanation of what's wrong ) diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx index 3fded5d4..89a635b5 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx @@ -1438,6 +1438,7 @@ export const SidebarChat = () => { role: 'assistant', content: messageSoFar ?? '', reasoning: reasoningSoFar ?? '', + anthropicReasoning: null, }} isLoading={isStreaming} /> : null diff --git a/src/vs/workbench/contrib/void/browser/toolsService.ts b/src/vs/workbench/contrib/void/browser/toolsService.ts index 40c55dbd..a598c254 100644 --- a/src/vs/workbench/contrib/void/browser/toolsService.ts +++ b/src/vs/workbench/contrib/void/browser/toolsService.ts @@ -395,7 +395,7 @@ export class ToolsService implements IToolsService { const fromIdx = MAX_FILE_CHARS_PAGE * (pageNumber - 1) const toIdx = MAX_FILE_CHARS_PAGE * pageNumber - 1 - const fileContents = readFileContents.slice(fromIdx, toIdx + 1) || '(empty)' // paginate + const fileContents = readFileContents.slice(fromIdx, toIdx + 1) // paginate const hasNextPage = (readFileContents.length - 1) - toIdx >= 1 return { fileContents, hasNextPage } }, diff --git a/src/vs/workbench/contrib/void/common/llmMessageTypes.ts b/src/vs/workbench/contrib/void/common/llmMessageTypes.ts index 5bac84cc..09830aee 100644 --- a/src/vs/workbench/contrib/void/common/llmMessageTypes.ts +++ b/src/vs/workbench/contrib/void/common/llmMessageTypes.ts @@ -36,6 +36,7 @@ export type LLMChatMessage = { } | { role: 'assistant', content: string; // text content + anthropicReasoning: AnthropicReasoning[] | null; } | { role: 'tool'; content: string; // result @@ -51,14 +52,14 @@ export type ToolCallType = { id: string; } +export type AnthropicReasoning = ({ type: 'thinking'; thinking: any; signature: string; } | { type: 'redacted_thinking', data: any }) export type OnText = (p: { fullText: string; fullReasoning: string }) => void -export type OnFinalMessage = (p: { fullText: string; fullReasoning: string; toolCalls?: ToolCallType[]; }) => void // id is tool_use_id +export type OnFinalMessage = (p: { fullText: string; fullReasoning: string; toolCalls?: ToolCallType[]; anthropicReasoning: AnthropicReasoning[] | null }) => void // id is tool_use_id export type OnError = (p: { message: string; fullError: Error | null }) => void export type AbortRef = { current: (() => void) | null } - export type LLMFIMMessage = { prefix: string; suffix: string; diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts index 007899cb..55f92819 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts @@ -3,7 +3,7 @@ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. *--------------------------------------------------------------------------------------*/ -import { LLMChatMessage, LLMFIMMessage } from '../../common/llmMessageTypes.js'; +import { AnthropicReasoning, LLMChatMessage, LLMFIMMessage } from '../../common/llmMessageTypes.js'; import { deepClone } from '../../../../../base/common/objects.js'; @@ -22,7 +22,7 @@ type InternalLLMChatMessage = { content: string; } | { role: 'assistant', - content: string | ({ type: 'text'; text: string })[]; + content: string | (AnthropicReasoning | { type: 'text'; text: string })[]; } | { role: 'tool'; content: string; // result @@ -35,7 +35,7 @@ type InternalLLMChatMessage = { const EMPTY_MESSAGE = '(empty message)' const EMPTY_TOOL_CONTENT = '(empty content)' -const prepareMessages_normalize = ({ messages: messages_ }: { messages: LLMChatMessage[] }) => { +const prepareMessages_normalize = ({ messages: messages_ }: { messages: LLMChatMessage[] }): { messages: LLMChatMessage[] } => { const messages = deepClone(messages_) const newMessages: LLMChatMessage[] = [] if (messages.length >= 0) newMessages.push(messages[0]) @@ -155,7 +155,7 @@ openai on developer system message - https://cdn.openai.com/spec/model-spec-2024 type PrepareMessagesToolsOpenAI = ( Exclude | { role: 'assistant', - content: string | { type: 'text'; text: string }[]; + content: string | (AnthropicReasoning | { type: 'text'; text: string })[]; tool_calls?: { type: 'function'; id: string; @@ -234,6 +234,7 @@ type PrepareMessagesToolsAnthropic = ( Exclude | { role: 'assistant', content: string | ( + | AnthropicReasoning | { type: 'text'; text: string; @@ -285,7 +286,7 @@ const prepareMessages_tools_anthropic = ({ messages }: { messages: InternalLLMCh newMessages[i] = { role: 'user', content: [ - ...[{ type: 'tool_result', tool_use_id: currMsg.id, content: currMsg.content }] as const, + ...[{ type: 'tool_result', tool_use_id: currMsg.id, content: currMsg.content || EMPTY_TOOL_CONTENT }] as const, ...currMsg.content ? [{ type: 'text', text: currMsg.content }] as const : [], ] } @@ -314,6 +315,28 @@ const prepareMessages_tools = ({ messages, supportsTools }: { messages: Internal } +// remove rawAnthropicAssistantContent, and make content equal to it if supportsAnthropicContent +const prepareMessages_anthropicContent = ({ messages, supportsAnthropicReasoningSignature }: { messages: LLMChatMessage[], supportsAnthropicReasoningSignature: boolean }) => { + const newMessages: InternalLLMChatMessage[] = [] + for (const m of messages) { + if (m.role !== 'assistant') { + newMessages.push(m) + continue + } + let newMessage: InternalLLMChatMessage + if (supportsAnthropicReasoningSignature && m.anthropicReasoning) { + const content = m.content ? [...m.anthropicReasoning, { type: 'text' as const, text: m.content }] : m.anthropicReasoning + newMessage = { role: 'assistant', content: content } + } + else { + newMessage = { role: 'assistant', content: m.content } + } + newMessages.push(newMessage) + } + return { messages: newMessages } +} + + @@ -347,18 +370,21 @@ export const prepareMessages = ({ aiInstructions, supportsSystemMessage, supportsTools, + supportsAnthropicReasoningSignature, }: { messages: LLMChatMessage[], aiInstructions: string, supportsSystemMessage: false | 'system-role' | 'developer-role' | 'separated', supportsTools: false | 'anthropic-style' | 'openai-style', + supportsAnthropicReasoningSignature: boolean, }) => { const { messages: messages1 } = prepareMessages_normalize({ messages }) - const { messages: messages2, separateSystemMessageStr } = prepareMessages_systemMessage({ messages: messages1, aiInstructions, supportsSystemMessage }) - const { messages: messages3 } = prepareMessages_tools({ messages: messages2, supportsTools }) - const { messages: messages4 } = prepareMessages_noEmptyMessage({ messages: messages3 }) + const { messages: messages2 } = prepareMessages_anthropicContent({ messages: messages1, supportsAnthropicReasoningSignature }) + const { messages: messages3, separateSystemMessageStr } = prepareMessages_systemMessage({ messages: messages2, aiInstructions, supportsSystemMessage }) + const { messages: messages4 } = prepareMessages_tools({ messages: messages3, supportsTools }) + const { messages: messages5 } = prepareMessages_noEmptyMessage({ messages: messages4 }) return { - messages: messages4 as any, + messages: messages5 as any, separateSystemMessageStr } as const } diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts index 925d5be3..901ff013 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts @@ -140,7 +140,7 @@ const _sendOpenAICompatibleFIM = ({ messages: messages_, onFinalMessage, onError }) .then(async response => { const fullText = response.choices[0]?.text - onFinalMessage({ fullText, fullReasoning: '' }); + onFinalMessage({ fullText, fullReasoning: '', anthropicReasoning: null }); }) .catch(error => { if (error instanceof OpenAI.APIError && error.status === 401) { onError({ message: invalidApiKeyMessage(providerName), fullError: error }); } @@ -168,7 +168,7 @@ const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage const { providerReasoningIOSettings } = getProviderCapabilities(providerName) - const { messages } = prepareMessages({ messages: messages_, aiInstructions, supportsSystemMessage, supportsTools }) + const { messages } = prepareMessages({ messages: messages_, aiInstructions, supportsSystemMessage, supportsTools, supportsAnthropicReasoningSignature: false }) const tools = (supportsTools && ((tools_?.length ?? 0) !== 0)) ? tools_?.map(tool => toOpenAICompatibleTool(tool)) : undefined const includeInPayload = canIOReasoning ? providerReasoningIOSettings?.input?.includeInPayload || {} : {} @@ -222,9 +222,9 @@ const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage else { if (manuallyParseReasoning) { const { fullText, fullReasoning } = extractReasoningOnFinalMessage(fullTextSoFar, openSourceThinkTags) - onFinalMessage({ fullText, fullReasoning, toolCalls }); + onFinalMessage({ fullText, fullReasoning, toolCalls, anthropicReasoning: null }); } else { - onFinalMessage({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar, toolCalls }); + onFinalMessage({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar, toolCalls, anthropicReasoning: null }); } } }) @@ -301,7 +301,7 @@ const sendAnthropicChat = ({ messages: messages_, providerName, onText, onFinalM reasoningBudget, } = getModelSelectionState(providerName, modelName_, optionsOfModelSelection) // user's modelName_ here - const { messages, separateSystemMessageStr } = prepareMessages({ messages: messages_, aiInstructions, supportsSystemMessage, supportsTools }) + const { messages, separateSystemMessageStr } = prepareMessages({ messages: messages_, aiInstructions, supportsSystemMessage, supportsTools, supportsAnthropicReasoningSignature: true }) console.log('MESSAGES!!!!', JSON.stringify(messages, null, 5)) @@ -374,7 +374,8 @@ const sendAnthropicChat = ({ messages: messages_, providerName, onText, onFinalM // on done - (or when error/fail) - this is called AFTER last streamEvent stream.on('finalMessage', (response) => { const toolCalls = toolCallsFrom_Anthropic(response.content) - onFinalMessage({ fullText, fullReasoning, toolCalls }) + const anthropicReasoning = response.content.filter(c => c.type === 'thinking' || c.type === 'redacted_thinking') + onFinalMessage({ fullText, fullReasoning, toolCalls, anthropicReasoning }) }) // on error stream.on('error', (error) => { @@ -458,7 +459,7 @@ const sendOllamaFIM = ({ messages: messages_, onFinalMessage, onError, settingsO const newText = chunk.response fullText += newText } - onFinalMessage({ fullText, fullReasoning: '' }) + onFinalMessage({ fullText, fullReasoning: '', anthropicReasoning: null }) }) // when error/fail .catch((error) => {