anthropic reasoning works with tools

This commit is contained in:
Andrew Pareles 2025-03-07 20:28:23 -08:00
parent f89b036d17
commit 7558d4dc1c
7 changed files with 62 additions and 34 deletions

View file

@ -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<ToolName>
| ToolRequestApproval<ToolName>
@ -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 {

View file

@ -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
)

View file

@ -1438,6 +1438,7 @@ export const SidebarChat = () => {
role: 'assistant',
content: messageSoFar ?? '',
reasoning: reasoningSoFar ?? '',
anthropicReasoning: null,
}}
isLoading={isStreaming}
/> : null

View file

@ -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 }
},

View file

@ -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;

View file

@ -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<InternalLLMChatMessage, { role: 'assistant' | 'tool' }> | {
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<InternalLLMChatMessage, { role: 'assistant' | 'user' }> | {
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
}

View file

@ -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) => {