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 88fff3b3..2f1c7ae6 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 @@ -1437,14 +1437,14 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, const start = toolMessage.params.startLine === null ? `start` : `${toolMessage.params.startLine}` const end = toolMessage.params.endLine === null ? `end` : `${toolMessage.params.endLine}` const addStr = `(${start}-${end})` - componentParams.title += ` ${addStr}` + componentParams.desc1 += ` ${addStr}` } if (toolMessage.type === 'success') { const { params, result } = toolMessage componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) } if (result.hasNextPage && params.pageNumber === 1) // first page - componentParams.desc2 = '(more content available)' + componentParams.desc2 = '(truncated)' else if (params.pageNumber > 1) // subsequent pages componentParams.desc2 = `(part ${params.pageNumber})` } diff --git a/src/vs/workbench/contrib/void/common/prompt/prompts.ts b/src/vs/workbench/contrib/void/common/prompt/prompts.ts index 04b809cf..57383bf3 100644 --- a/src/vs/workbench/contrib/void/common/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/common/prompt/prompts.ts @@ -629,39 +629,6 @@ ${tripleTick[1]}).` -// const toAnthropicTool = (toolInfo: InternalToolInfo) => { -// const { name, description, params } = toolInfo -// return { -// name: name, -// description: description, -// input_schema: { -// type: 'object', -// properties: params, -// // required: Object.keys(params), -// }, -// } satisfies Anthropic.Messages.Tool -// } - - -// const toOpenAICompatibleTool = (toolInfo: InternalToolInfo) => { -// const { name, description, params } = toolInfo -// return { -// type: 'function', -// function: { -// name: name, -// // strict: true, // strict mode - https://platform.openai.com/docs/guides/function-calling?api-mode=chat -// description: description, -// parameters: { -// type: 'object', -// properties: params, -// // required: Object.keys(params), // in strict mode, all params are required and additionalProperties is false -// // additionalProperties: false, -// }, -// } -// } satisfies OpenAI.Chat.Completions.ChatCompletionTool -// } - - /* // ======================================================== ai search/replace ======================================================== diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/extractGrammar.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/extractGrammar.ts index ed3e20fa..3e2031f1 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/extractGrammar.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/extractGrammar.ts @@ -135,7 +135,7 @@ export const extractReasoningWrapper = ( } -// =============== tools =============== +// =============== tools (XML) =============== @@ -256,7 +256,7 @@ const parseXMLPrefixToToolCall = (toolName: ToolName, toolId: string, str: strin } } -export const extractToolsWrapper = ( +export const extractXMLToolsWrapper = ( onText: OnText, onFinalMessage: OnFinalMessage, chatMode: ChatMode | null ): { newOnText: OnText, newOnFinalMessage: OnFinalMessage } => { 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 3db03a91..5f7f0474 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 @@ -10,10 +10,11 @@ import { MistralCore } from '@mistralai/mistralai/core.js'; import { fimComplete } from '@mistralai/mistralai/funcs/fimComplete.js'; -import { AnthropicLLMChatMessage, LLMChatMessage, LLMFIMMessage, ModelListParams, OllamaModelResponse, OnError, OnFinalMessage, OnText } from '../../common/sendLLMMessageTypes.js'; +import { AnthropicLLMChatMessage, LLMChatMessage, LLMFIMMessage, ModelListParams, OllamaModelResponse, OnError, OnFinalMessage, OnText, RawToolCallObj, RawToolParamsObj } from '../../common/sendLLMMessageTypes.js'; import { ChatMode, displayInfoOfProviderName, ModelSelectionOptions, ProviderName, SettingsOfProvider } from '../../common/voidSettingsTypes.js'; import { getSendableReasoningInfo, getModelCapabilities, getProviderCapabilities, defaultProviderSettings, getMaxOutputTokens } from '../../common/modelCapabilities.js'; -import { extractReasoningWrapper, extractToolsWrapper } from './extractGrammar.js'; +import { extractReasoningWrapper, extractXMLToolsWrapper } from './extractGrammar.js'; +import { availableTools, InternalToolInfo, isAToolName, ToolParamName, voidTools } from '../../common/prompt/prompts.js'; type InternalCommonMessageParams = { @@ -124,6 +125,55 @@ const _sendOpenAICompatibleFIM = ({ messages: { prefix, suffix, stopTokens }, on } +const toOpenAICompatibleTool = (toolInfo: InternalToolInfo) => { + const { name, description, params } = toolInfo + return { + type: 'function', + function: { + name: name, + // strict: true, // strict mode - https://platform.openai.com/docs/guides/function-calling?api-mode=chat + description: description, + parameters: { + type: 'object', + properties: params, + // required: Object.keys(params), // in strict mode, all params are required and additionalProperties is false + // additionalProperties: false, + }, + } + } satisfies OpenAI.Chat.Completions.ChatCompletionTool +} + +const openAITools = (chatMode: ChatMode) => { + const allowedTools = availableTools(chatMode) + if (!allowedTools || Object.keys(allowedTools).length === 0) return null + + const openAITools: OpenAI.Chat.Completions.ChatCompletionTool[] = [] + for (const t in allowedTools ?? {}) { + openAITools.push(toOpenAICompatibleTool(allowedTools[t])) + } + return openAITools +} + +const openAIToolToRawToolCallObj = (name: string, toolParamsStr: string, id: string): RawToolCallObj | null => { + if (!isAToolName(name)) return null + const rawParams: RawToolParamsObj = {} + let input: unknown + try { + input = JSON.parse(toolParamsStr) + } + catch (e) { + return null + } + if (input === null) return null + if (typeof input !== 'object') return null + for (const paramName in voidTools[name].params) { + rawParams[paramName as ToolParamName] = (input as any)[paramName] + } + return { id, name, rawParams, doneParams: Object.keys(rawParams) as ToolParamName[], isDone: true } +} + + +// ------------ OPENAI-COMPATIBLE ------------ const _sendOpenAICompatibleChat = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName: modelName_, _setAborter, providerName, chatMode, separateSystemMessage }: SendChatParams_Internal) => { @@ -140,12 +190,17 @@ const _sendOpenAICompatibleChat = ({ messages, onText, onFinalMessage, onError, const reasoningInfo = getSendableReasoningInfo('Chat', providerName, modelName_, modelSelectionOptions) // user's modelName_ here const includeInPayload = providerReasoningIOSettings?.input?.includeInPayload?.(reasoningInfo) || {} + // tools + const potentialTools = chatMode !== null ? openAITools(chatMode) : null + const nativeToolsObj = potentialTools ? { tools: potentialTools } as const : {} + // instance const openai: OpenAI = newOpenAICompatibleSDK({ providerName, settingsOfProvider, includeInPayload }) const options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = { model: modelName, messages: messages as any, stream: true, + ...nativeToolsObj, // max_completion_tokens: maxTokens, } @@ -160,7 +215,7 @@ const _sendOpenAICompatibleChat = ({ messages, onText, onFinalMessage, onError, // manually parse out tool results if XML if (!specialToolFormat) { - const { newOnText, newOnFinalMessage } = extractToolsWrapper(onText, onFinalMessage, chatMode) + const { newOnText, newOnFinalMessage } = extractXMLToolsWrapper(onText, onFinalMessage, chatMode) onText = newOnText onFinalMessage = newOnFinalMessage } @@ -168,6 +223,10 @@ const _sendOpenAICompatibleChat = ({ messages, onText, onFinalMessage, onError, let fullReasoningSoFar = '' let fullTextSoFar = '' + let toolName = '' + let toolId = '' + let toolParamsStr = '' + openai.chat.completions .create(options) .then(async response => { @@ -178,6 +237,17 @@ const _sendOpenAICompatibleChat = ({ messages, onText, onFinalMessage, onError, const newText = chunk.choices[0]?.delta?.content ?? '' fullTextSoFar += newText + // tool call + for (const tool of chunk.choices[0]?.delta?.tool_calls ?? []) { + const index = tool.index + if (index !== 0) continue + + toolName += tool.function?.name ?? '' + toolParamsStr += tool.function?.arguments ?? ''; + toolId += tool.id ?? '' + } + + // reasoning let newReasoning = '' if (nameOfReasoningFieldInDelta) { @@ -189,11 +259,12 @@ const _sendOpenAICompatibleChat = ({ messages, onText, onFinalMessage, onError, onText({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar }) } // on final - if (!fullTextSoFar && !fullReasoningSoFar) { + if (!fullTextSoFar && !fullReasoningSoFar && !toolName) { onError({ message: 'Void: Response from model was empty.', fullError: null }) } else { - onFinalMessage({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar, anthropicReasoning: null }); + const toolCall = openAIToolToRawToolCallObj(toolName, toolParamsStr, toolId) ?? undefined + onFinalMessage({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar, anthropicReasoning: null, toolCall: toolCall }); } }) // when error/fail - this catches errors of both .create() and .then(for await) @@ -241,6 +312,43 @@ const _openaiCompatibleList = async ({ onSuccess: onSuccess_, onError: onError_, +// ------------ ANTHROPIC (HELPERS) ------------ +const toAnthropicTool = (toolInfo: InternalToolInfo) => { + const { name, description, params } = toolInfo + return { + name: name, + description: description, + input_schema: { + type: 'object', + properties: params, + // required: Object.keys(params), + }, + } satisfies Anthropic.Messages.Tool +} + +const anthropicTools = (chatMode: ChatMode) => { + const allowedTools = availableTools(chatMode) + if (!allowedTools || Object.keys(allowedTools).length === 0) return null + + const anthropicTools: Anthropic.Messages.ToolUnion[] = [] + for (const t in allowedTools ?? {}) { + anthropicTools.push(toAnthropicTool(allowedTools[t])) + } + return anthropicTools +} + +const anthropicToolToRawToolCallObj = (toolBlock: Anthropic.Messages.ToolUseBlock): RawToolCallObj | null => { + const { id, name, input } = toolBlock + if (!isAToolName(name)) return null + const rawParams: RawToolParamsObj = {} + if (input === null) return null + if (typeof input !== 'object') return null + for (const paramName in voidTools[name].params) { + rawParams[paramName as ToolParamName] = (input as any)[paramName] + } + return { id, name, rawParams, doneParams: Object.keys(rawParams) as ToolParamName[], isDone: true } +} + // ------------ ANTHROPIC ------------ const sendAnthropicChat = ({ messages, providerName, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName: modelName_, _setAborter, separateSystemMessage, chatMode }: SendChatParams_Internal) => { const { @@ -258,6 +366,11 @@ const sendAnthropicChat = ({ messages, providerName, onText, onFinalMessage, onE // anthropic-specific - max tokens const maxTokens = getMaxOutputTokens(providerName, modelName_, { isReasoningEnabled: !!reasoningInfo?.isReasoningEnabled }) + // tools + const potentialTools = chatMode !== null ? anthropicTools(chatMode) : null + const nativeToolsObj = potentialTools ? { tools: potentialTools, tool_choice: { type: 'auto' } } as const : {} + + // instance const anthropic = new Anthropic({ apiKey: thisConfig.apiKey, @@ -270,11 +383,13 @@ const sendAnthropicChat = ({ messages, providerName, onText, onFinalMessage, onE model: modelName, max_tokens: maxTokens ?? 4_096, // anthropic requires this ...includeInPayload, + ...nativeToolsObj, + }) // manually parse out tool results if (!specialToolFormat) { - const { newOnText, newOnFinalMessage } = extractToolsWrapper(onText, onFinalMessage, chatMode) + const { newOnText, newOnFinalMessage } = extractXMLToolsWrapper(onText, onFinalMessage, chatMode) onText = newOnText onFinalMessage = newOnFinalMessage } @@ -283,8 +398,8 @@ const sendAnthropicChat = ({ messages, providerName, onText, onFinalMessage, onE 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 => { @@ -306,10 +421,10 @@ const sendAnthropicChat = ({ messages, providerName, onText, onFinalMessage, onE 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 @@ -322,17 +437,19 @@ const sendAnthropicChat = ({ messages, providerName, onText, onFinalMessage, onE 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, }) + } } }) // on done - (or when error/fail) - this is called AFTER last streamEvent stream.on('finalMessage', (response) => { const anthropicReasoning = response.content.filter(c => c.type === 'thinking' || c.type === 'redacted_thinking') - onFinalMessage({ fullText, fullReasoning, anthropicReasoning }) + const tools = response.content.filter(c => c.type === 'tool_use') + const toolCall = tools[0] ? anthropicToolToRawToolCallObj(tools[0]) ?? undefined : undefined + onFinalMessage({ fullText, fullReasoning, anthropicReasoning, toolCall, }) }) // on error stream.on('error', (error) => {