From ee43ec99ea855bbf395b7a5976fb71a776d004d7 Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Mon, 14 Apr 2025 13:59:30 -0700 Subject: [PATCH 01/12] duplicate tooltips --- .../react/src/void-settings-tsx/Settings.tsx | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx index 776bb72c..7be39837 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx @@ -312,12 +312,11 @@ export const ModelDump = () => { {/* right part is anything that fits */}
{isAutodetected ? '(detected locally)' : isDefault ? '' : '(custom model)'} @@ -616,7 +615,19 @@ export const FeaturesTab = () => { {/* FIM */}

{displayInfoOfFeatureName('Autocomplete')}

-
Experimental. Only works with models that support FIM.
+
+ + Experimental. Only works with FIM models. + + + * + +
{/* Enable Switch */} From e6ecad3f65d7d52591c9104d9ebd9b91c92ceae4 Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Mon, 14 Apr 2025 14:34:04 -0700 Subject: [PATCH 02/12] desc --- .../void/browser/react/src/void-settings-tsx/Settings.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx index 7be39837..dd8914fb 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx @@ -707,7 +707,7 @@ export const FeaturesTab = () => { value={voidSettingsState.globalSettings.includeToolLintErrors} onChange={(newVal) => voidSettingsService.setGlobalSetting('includeToolLintErrors', newVal)} /> - {voidSettingsState.globalSettings.includeToolLintErrors ? 'Include after-edit lint errors' : `Don't include lint errors`} + {voidSettingsState.globalSettings.includeToolLintErrors ? 'Fix lint errors' : `Don't fix lint errors`}
From 77c59f2e801f25c2f5c170310ad049426f85d661 Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Mon, 14 Apr 2025 14:41:48 -0700 Subject: [PATCH 03/12] lint error severity must be warning or error --- src/vs/workbench/contrib/void/browser/toolsService.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/toolsService.ts b/src/vs/workbench/contrib/void/browser/toolsService.ts index 9bc53227..53a33daf 100644 --- a/src/vs/workbench/contrib/void/browser/toolsService.ts +++ b/src/vs/workbench/contrib/void/browser/toolsService.ts @@ -14,7 +14,7 @@ import { EndOfLinePreference } from '../../../../editor/common/model.js' import { basename } from '../../../../base/common/path.js' import { IVoidCommandBarService } from './voidCommandBarService.js' import { computeDirectoryTree1Deep, IDirectoryStrService, stringifyDirectoryTree1Deep } from './directoryStrService.js' -import { IMarkerService } from '../../../../platform/markers/common/markers.js' +import { IMarkerService, MarkerSeverity } from '../../../../platform/markers/common/markers.js' import { timeout } from '../../../../base/common/async.js' import { RawToolParamsObj } from '../common/sendLLMMessageTypes.js' import { ToolName } from '../common/prompt/prompts.js' @@ -471,9 +471,10 @@ export class ToolsService implements IToolsService { private _getLintErrors(uri: URI): { lintErrors: LintErrorItem[] | null } { const lintErrors = this.markerService .read({ resource: uri }) + .filter(l => l.severity === MarkerSeverity.Error || l.severity === MarkerSeverity.Warning) .map(l => ({ code: typeof l.code === 'string' ? l.code : l.code?.value || '', - message: l.message, + message: (l.severity === MarkerSeverity.Error ? '(error) ' : '(warning) ') + l.message, startLineNumber: l.startLineNumber, endLineNumber: l.endLineNumber, } satisfies LintErrorItem)) From d2098f71ebac5063f9eb437dfb9b61c5bc24c3d4 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Mon, 14 Apr 2025 22:01:46 -0700 Subject: [PATCH 04/12] support 4.1 native tools and 3.7 native thinking... --- .../void/browser/autocompleteService.ts | 17 +- .../contrib/void/browser/chatThreadService.ts | 125 +--- .../browser/convertToLLMMessageService.ts | 607 ++++++++++++++++++ .../contrib/void/browser/editCodeService.ts | 61 +- .../react/src/sidebar-tsx/SidebarChat.tsx | 1 - .../void/browser/searchReplaceCacheService.ts | 44 -- .../void/common/chatThreadServiceTypes.ts | 9 +- .../contrib/void/common/modelCapabilities.ts | 29 + .../contrib/void/common/prompt/prompts.ts | 17 +- .../void/common/sendLLMMessageService.ts | 6 +- .../void/common/sendLLMMessageTypes.ts | 52 +- .../llmMessage/extractGrammar.ts | 16 +- .../llmMessage/preprocessLLMMessages.ts | 524 --------------- .../llmMessage/sendLLMMessage.impl.ts | 65 +- .../llmMessage/sendLLMMessage.ts | 6 +- 15 files changed, 803 insertions(+), 776 deletions(-) create mode 100644 src/vs/workbench/contrib/void/browser/convertToLLMMessageService.ts delete mode 100644 src/vs/workbench/contrib/void/browser/searchReplaceCacheService.ts delete mode 100644 src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts diff --git a/src/vs/workbench/contrib/void/browser/autocompleteService.ts b/src/vs/workbench/contrib/void/browser/autocompleteService.ts index e1832646..5bf1200d 100644 --- a/src/vs/workbench/contrib/void/browser/autocompleteService.ts +++ b/src/vs/workbench/contrib/void/browser/autocompleteService.ts @@ -20,6 +20,7 @@ import { ILLMMessageService } from '../common/sendLLMMessageService.js'; import { isWindows } from '../../../../base/common/platform.js'; import { IVoidSettingsService } from '../common/voidSettingsService.js'; import { FeatureName } from '../common/voidSettingsTypes.js'; +import { IConvertToLLMMessageService } from './convertToLLMMessageService.js'; // import { IContextGatheringService } from './contextGatheringService.js'; @@ -791,18 +792,21 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ const featureName: FeatureName = 'Autocomplete' const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName] const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[featureName][modelSelection.providerName]?.[modelSelection.modelName] : undefined - + const aiInstructions = this._settingsService.state.globalSettings.aiInstructions // set parameters of `newAutocompletion` appropriately newAutocompletion.llmPromise = new Promise((resolve, reject) => { const requestId = this._llmMessageService.sendLLMMessage({ messagesType: 'FIMMessage', - messages: { - prefix: llmPrefix, - suffix: llmSuffix, - stopTokens: stopTokens, - }, + messages: this._convertToLLMMessageService.prepareFIMMessage({ + messages: { + prefix: llmPrefix, + suffix: llmSuffix, + stopTokens: stopTokens, + }, + aiInstructions + }), modelSelection, modelSelectionOptions, logging: { loggingName: 'Autocomplete' }, @@ -890,6 +894,7 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ @IEditorService private readonly _editorService: IEditorService, @IModelService private readonly _modelService: IModelService, @IVoidSettingsService private readonly _settingsService: IVoidSettingsService, + @IConvertToLLMMessageService private readonly _convertToLLMMessageService: IConvertToLLMMessageService // @IContextGatheringService private readonly _contextGatheringService: IContextGatheringService, ) { super() diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index ac4608c6..34b499ca 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -11,11 +11,10 @@ import { IStorageService, StorageScope, StorageTarget } from '../../../../platfo import { URI } from '../../../../base/common/uri.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { ILLMMessageService } from '../common/sendLLMMessageService.js'; -import { chat_userMessageContent, chat_systemMessage, ToolName, toolCallXMLStr, } from '../common/prompt/prompts.js'; -import { getErrorMessage, LLMChatMessage, RawToolCallObj, RawToolParamsObj } from '../common/sendLLMMessageTypes.js'; -import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; +import { chat_userMessageContent, ToolName, } from '../common/prompt/prompts.js'; +import { getErrorMessage, RawToolCallObj, RawToolParamsObj } from '../common/sendLLMMessageTypes.js'; import { generateUuid } from '../../../../base/common/uuid.js'; -import { ChatMode, FeatureName, ModelSelection, ModelSelectionOptions } from '../common/voidSettingsTypes.js'; +import { FeatureName, ModelSelection, ModelSelectionOptions } from '../common/voidSettingsTypes.js'; import { IVoidSettingsService } from '../common/voidSettingsService.js'; import { ToolCallParams, ToolResultType, toolNamesThatRequireApproval } from '../common/toolsServiceTypes.js'; import { IToolsService } from './toolsService.js'; @@ -23,7 +22,6 @@ import { CancellationToken } from '../../../../base/common/cancellation.js'; import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js'; import { ChatMessage, CheckpointEntry, CodespanLocationLink, StagingSelectionItem, ToolMessage } from '../common/chatThreadServiceTypes.js'; import { Position } from '../../../../editor/common/core/position.js'; -import { ITerminalToolService } from './terminalToolService.js'; import { IMetricsService } from '../common/metricsService.js'; import { shorten } from '../../../../base/common/labels.js'; import { IVoidModelService } from '../common/voidModelService.js'; @@ -33,11 +31,9 @@ import { findLast, findLastIdx } from '../../../../base/common/arraysFind.js'; import { IEditCodeService } from './editCodeServiceInterface.js'; import { VoidFileSnapshot } from '../common/editCodeServiceTypes.js'; import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js'; -import { IModelService } from '../../../../editor/common/services/model.js'; -import { IDirectoryStrService } from './directoryStrService.js'; import { truncate } from '../../../../base/common/strings.js'; import { THREAD_STORAGE_KEY } from '../common/storageKeys.js'; -import { deepClone } from '../../../../base/common/objects.js'; +import { IConvertToLLMMessageService } from './convertToLLMMessageService.js'; /* @@ -221,17 +217,14 @@ class ChatThreadService extends Disposable implements IChatThreadService { @IVoidModelService private readonly _voidModelService: IVoidModelService, @ILLMMessageService private readonly _llmMessageService: ILLMMessageService, @IToolsService private readonly _toolsService: IToolsService, - @IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService, @IVoidSettingsService private readonly _settingsService: IVoidSettingsService, @ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService, - @ITerminalToolService private readonly _terminalToolService: ITerminalToolService, @IMetricsService private readonly _metricsService: IMetricsService, @IEditorService private readonly _editorService: IEditorService, @ICodeEditorService private readonly _codeEditorService: ICodeEditorService, @IEditCodeService private readonly _editCodeService: IEditCodeService, @INotificationService private readonly _notificationService: INotificationService, - @IModelService private readonly _modelService: IModelService, - @IDirectoryStrService private readonly _directoryStrService: IDirectoryStrService, + @IConvertToLLMMessageService private readonly _convertToLLMMessagesService: IConvertToLLMMessageService, ) { super() this.state = { allThreads: {}, currentThreadId: null as unknown as string } // default state @@ -454,10 +447,10 @@ class ChatThreadService extends Disposable implements IChatThreadService { } else return - const { name } = lastMsg + const { name, id, rawParams } = lastMsg const errorMessage = this.errMsgs.rejected - this._updateLatestTool(threadId, { role: 'tool', type: 'rejected', params: params, name: name, content: errorMessage, result: null }) + this._updateLatestTool(threadId, { role: 'tool', type: 'rejected', params: params, name: name, content: errorMessage, result: null, id, rawParams }) this._setStreamState(threadId, {}, 'set') } @@ -482,7 +475,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { const llmCancelToken = this.streamState[threadId]?.streamingToken if (llmCancelToken !== undefined) { this._llmMessageService.abort(llmCancelToken) } - this._addMessageToThread(threadId, { role: 'assistant', displayContent: displayContentSoFar, reasoning: reasoningSoFar, toolCall: toolCallSoFar, anthropicReasoning: null }) + this._addMessageToThread(threadId, { role: 'assistant', displayContent: displayContentSoFar, reasoning: reasoningSoFar, anthropicReasoning: null }) if (toolCallSoFar) { this._addMessageToThread(threadId, { role: 'interrupted_streaming_tool', name: toolCallSoFar.name }) @@ -505,69 +498,12 @@ class ChatThreadService extends Disposable implements IChatThreadService { private readonly _currentlyRunningToolInterruptor: { [threadId: string]: (() => void) | undefined } = {} - - // system message - private _generateSystemMessage = async (chatMode: ChatMode) => { - const workspaceFolders = this._workspaceContextService.getWorkspace().folders.map(f => f.uri.fsPath) - - const openedURIs = this._modelService.getModels().filter(m => m.isAttachedToEditor()).map(m => m.uri.fsPath) || []; - const activeURI = this._editorService.activeEditor?.resource?.fsPath; - - const directoryStr = await this._directoryStrService.getAllDirectoriesStr({ - cutOffMessage: chatMode === 'agent' || chatMode === 'gather' ? `...Directories string cut off, use tools to read more...` - : `...Directories string cut off, ask user for more if necessary...` - }) - - const runningTerminalIds = this._terminalToolService.listTerminalIds() - const systemMessage = chat_systemMessage({ workspaceFolders, openedURIs, directoryStr, activeURI, runningTerminalIds, chatMode }) - return systemMessage - } - - private _generateLLMMessages = async (threadId: string) => { - const thread = this.state.allThreads[threadId] - if (!thread) return [] - - const chatMessages = deepClone(thread.messages) - const llmChatMessages: LLMChatMessage[] = [] - - // merge tools into user message - for (const c of chatMessages) { - if (c.role === 'assistant') { - // if called a tool, re-add its XML to the message - // alternatively, could just hold onto the original output, but this way requires less piping raw strings everywhere - let content = c.displayContent - if (c.toolCall) { - content = `${content}\n\n${toolCallXMLStr(c.toolCall)}` - } - llmChatMessages.push({ role: c.role, content: content, anthropicReasoning: c.anthropicReasoning }) - } - else if (c.role === 'user' || c.role === 'tool') { - if (c.role === 'tool') - c.content = `<${c.name}_result>\n${c.content}\n` - - if (llmChatMessages.length === 0 || llmChatMessages[llmChatMessages.length - 1].role !== 'user') - llmChatMessages.push({ role: 'user', content: c.content }) - else - llmChatMessages[llmChatMessages.length - 1].content += '\n\n' + c.content - - } - else if (c.role === 'interrupted_streaming_tool') { // pass - } - else if (c.role === 'checkpoint') { // pass - } - else { - throw new Error(`Role ${(c as any).role} not recognized.`) - } - } - return llmChatMessages - } - - // returns true when the tool call is waiting for user approval private _runToolCall = async ( threadId: string, toolName: ToolName, - opts: { preapproved: true, validatedParams: ToolCallParams[ToolName] } | { preapproved: false, unvalidatedToolParams: RawToolParamsObj }, + toolId: string, + opts: { preapproved: true, unvalidatedToolParams: RawToolParamsObj, validatedParams: ToolCallParams[ToolName] } | { preapproved: false, unvalidatedToolParams: RawToolParamsObj }, ): Promise<{ awaitingUserApproval?: boolean, interrupted?: boolean }> => { // compute these below @@ -582,7 +518,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { toolParams = params } catch (error) { const errorMessage = getErrorMessage(error) - this._addMessageToThread(threadId, { role: 'tool', type: 'invalid_params', params: null, result: null, name: toolName, content: errorMessage, }) + this._addMessageToThread(threadId, { role: 'tool', type: 'invalid_params', rawParams: opts.unvalidatedToolParams, result: null, name: toolName, content: errorMessage, id: toolId, }) return {} } // once validated, add checkpoint for edit @@ -593,7 +529,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { if (toolRequiresApproval) { const autoApprove = this._settingsService.state.globalSettings.autoApprove // add a tool_request because we use it for UI if a tool is loading (this should be improved in the future) - this._addMessageToThread(threadId, { role: 'tool', type: 'tool_request', content: '(never)', result: null, name: toolName, params: toolParams }) + this._addMessageToThread(threadId, { role: 'tool', type: 'tool_request', content: '(never)', result: null, name: toolName, params: toolParams, id: toolId, rawParams: opts.unvalidatedToolParams }) if (!autoApprove) { return { awaitingUserApproval: true } } @@ -603,9 +539,11 @@ class ChatThreadService extends Disposable implements IChatThreadService { toolParams = opts.validatedParams } + + // 3. call the tool this._setStreamState(threadId, { isRunning: 'tool' }, 'merge') - this._updateLatestTool(threadId, { role: 'tool', type: 'running_now', name: toolName, params: toolParams, content: '(value not received yet...)', result: null }) + this._updateLatestTool(threadId, { role: 'tool', type: 'running_now', name: toolName, params: toolParams, content: '(value not received yet...)', result: null, id: toolId, rawParams: opts.unvalidatedToolParams }) let interrupted = false try { @@ -624,7 +562,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { const errorMessage = getErrorMessage(error) - this._updateLatestTool(threadId, { role: 'tool', type: 'tool_error', params: toolParams, result: errorMessage, name: toolName, content: errorMessage, }) + this._updateLatestTool(threadId, { role: 'tool', type: 'tool_error', params: toolParams, result: errorMessage, name: toolName, content: errorMessage, id: toolId, rawParams: opts.unvalidatedToolParams }) return {} } @@ -633,12 +571,12 @@ class ChatThreadService extends Disposable implements IChatThreadService { toolResultStr = this._toolsService.stringOfResult[toolName](toolParams as any, toolResult as any) } catch (error) { const errorMessage = this.errMsgs.errWhenStringifying(error) - this._updateLatestTool(threadId, { role: 'tool', type: 'tool_error', params: toolParams, result: errorMessage, name: toolName, content: errorMessage, }) + this._updateLatestTool(threadId, { role: 'tool', type: 'tool_error', params: toolParams, result: errorMessage, name: toolName, content: errorMessage, id: toolId, rawParams: opts.unvalidatedToolParams }) return {} } // 5. add to history and keep going - this._updateLatestTool(threadId, { role: 'tool', type: 'success', params: toolParams, result: toolResult, name: toolName, content: toolResultStr, }) + this._updateLatestTool(threadId, { role: 'tool', type: 'success', params: toolParams, result: toolResult, name: toolName, content: toolResultStr, id: toolId, rawParams: opts.unvalidatedToolParams }) return {} }; @@ -659,6 +597,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { callThisToolFirst?: ToolMessage & { type: 'tool_request' } }) { + // above just defines helpers, below starts the actual function const { chatMode } = this._settingsService.state.globalSettings // should not change as we loop even if user changes it, so it goes here @@ -672,7 +611,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { // before enter loop, call tool if (callThisToolFirst) { - const { interrupted } = await this._runToolCall(threadId, callThisToolFirst.name, { preapproved: true, validatedParams: callThisToolFirst.params }) + const { interrupted } = await this._runToolCall(threadId, callThisToolFirst.name, callThisToolFirst.id, { preapproved: true, unvalidatedToolParams: callThisToolFirst.rawParams, validatedParams: callThisToolFirst.params }) if (interrupted) return } @@ -688,34 +627,36 @@ class ChatThreadService extends Disposable implements IChatThreadService { // send llm message this._setStreamState(threadId, { isRunning: 'LLM' }, 'merge') - const systemMessage = await this._generateSystemMessage(chatMode) - const llmMessages = await this._generateLLMMessages(threadId) - const messages: LLMChatMessage[] = [ - { role: 'system', content: systemMessage }, - ...llmMessages - ] + + const chatMessages = this.state.allThreads[threadId]?.messages ?? [] + const { messages, separateSystemMessage } = await this._convertToLLMMessagesService.prepareLLMChatMessages({ + chatMessages, + modelSelection, + chatMode + }) const llmCancelToken = this._llmMessageService.sendLLMMessage({ messagesType: 'chatMessages', chatMode, - messages, + messages: messages, modelSelection, modelSelectionOptions, logging: { loggingName: `Chat - ${chatMode}`, loggingExtras: { threadId, nMessagesSent, chatMode } }, + separateSystemMessage: separateSystemMessage, onText: ({ fullText, fullReasoning, toolCall }) => { this._setStreamState(threadId, { displayContentSoFar: fullText, reasoningSoFar: fullReasoning, toolCallSoFar: toolCall }, 'merge') }, onFinalMessage: async ({ fullText, fullReasoning, toolCall, anthropicReasoning, }) => { - this._addMessageToThread(threadId, { role: 'assistant', displayContent: fullText, reasoning: fullReasoning, toolCall, anthropicReasoning }) + this._addMessageToThread(threadId, { role: 'assistant', displayContent: fullText, reasoning: fullReasoning, anthropicReasoning }) this._setStreamState(threadId, { displayContentSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined, toolCallSoFar: undefined }, 'merge') resMessageIsDonePromise(toolCall) // resolve with tool calls }, onError: (error) => { const messageSoFar = this.streamState[threadId]?.displayContentSoFar ?? '' const reasoningSoFar = this.streamState[threadId]?.reasoningSoFar ?? '' - const toolCallSoFar = this.streamState[threadId]?.toolCallSoFar + // const toolCallSoFar = this.streamState[threadId]?.toolCallSoFar // add assistant's message to chat history, and clear selection - this._addMessageToThread(threadId, { role: 'assistant', displayContent: messageSoFar, reasoning: reasoningSoFar, toolCall: toolCallSoFar, anthropicReasoning: null }) + this._addMessageToThread(threadId, { role: 'assistant', displayContent: messageSoFar, reasoning: reasoningSoFar, anthropicReasoning: null }) this._setStreamState(threadId, { error }, 'set') resMessageIsDonePromise() }, @@ -742,7 +683,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { // call tool if there is one const tool: RawToolCallObj | undefined = toolCall if (tool) { - const { awaitingUserApproval, interrupted } = await this._runToolCall(threadId, tool.name, { preapproved: false, unvalidatedToolParams: tool.rawParams }) + const { awaitingUserApproval, interrupted } = await this._runToolCall(threadId, tool.name, tool.id, { preapproved: false, unvalidatedToolParams: tool.rawParams }) // stop if interrupted. we don't have to do this for llmMessage because we have a stream token for it and onAbort gets called, but we don't have the equivalent for tools. // just detect tool interruption which is the same as chat interruption right now diff --git a/src/vs/workbench/contrib/void/browser/convertToLLMMessageService.ts b/src/vs/workbench/contrib/void/browser/convertToLLMMessageService.ts new file mode 100644 index 00000000..3d64033e --- /dev/null +++ b/src/vs/workbench/contrib/void/browser/convertToLLMMessageService.ts @@ -0,0 +1,607 @@ +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { deepClone } from '../../../../base/common/objects.js'; +import { IModelService } from '../../../../editor/common/services/model.js'; +import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js'; +import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; +import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; +import { IEditorService } from '../../../services/editor/common/editorService.js'; +import { ChatMessage } from '../common/chatThreadServiceTypes.js'; +import { getIsReasoningEnabledState, getMaxOutputTokens, getModelCapabilities } from '../common/modelCapabilities.js'; +import { toolCallXMLStr, chat_systemMessage, ToolName } from '../common/prompt/prompts.js'; +import { AnthropicLLMChatMessage, AnthropicReasoning, LLMChatMessage, LLMFIMMessage, OpenAILLMChatMessage, RawToolParamsObj } from '../common/sendLLMMessageTypes.js'; +import { IVoidSettingsService } from '../common/voidSettingsService.js'; +import { ChatMode, FeatureName, ModelSelection } from '../common/voidSettingsTypes.js'; +import { IDirectoryStrService } from './directoryStrService.js'; +import { ITerminalToolService } from './terminalToolService.js'; + + + + +type SimpleLLMMessage = { + role: 'tool'; + content: string; + id: string; + name: ToolName; + rawParams: RawToolParamsObj; +} | { + role: 'user'; + content: string; +} | { + role: 'assistant'; + content: string; + anthropicReasoning: AnthropicReasoning[] | null; +} + + + + +const EMPTY_MESSAGE = '(empty message)' + +const CHARS_PER_TOKEN = 4 +const TRIM_TO_LEN = 60 + + + + +// convert messages as if about to send to openai +/* +reference - https://platform.openai.com/docs/guides/function-calling#function-calling-steps +openai MESSAGE (role=assistant): +"tool_calls":[{ + "type": "function", + "id": "call_12345xyz", + "function": { + "name": "get_weather", + "arguments": "{\"latitude\":48.8566,\"longitude\":2.3522}" +}] + +openai RESPONSE (role=user): +{ "role": "tool", + "tool_call_id": tool_call.id, + "content": str(result) } + +also see +openai on prompting - https://platform.openai.com/docs/guides/reasoning#advice-on-prompting +openai on developer system message - https://cdn.openai.com/spec/model-spec-2024-05-08.html#follow-the-chain-of-command +*/ + + +const prepareMessages_openai_tools = (messages: SimpleLLMMessage[]): LLMChatMessage[] => { + + const newMessages: OpenAILLMChatMessage[] = []; + + for (let i = 0; i < messages.length; i += 1) { + const currMsg = messages[i] + + if (currMsg.role !== 'tool') { + newMessages.push(currMsg) + continue + } + + // edit previous assistant message to have called the tool + const prevMsg = 0 <= i - 1 && i - 1 <= newMessages.length ? newMessages[i - 1] : undefined + if (prevMsg?.role === 'assistant') { + prevMsg.tool_calls = [{ + type: 'function', + id: currMsg.id, + function: { + name: currMsg.name, + arguments: JSON.stringify(currMsg.rawParams) + } + }] + } + + // add the tool + newMessages.push({ + role: 'tool', + tool_call_id: currMsg.id, + content: currMsg.content, + }) + } + return newMessages + +} + + + +// convert messages as if about to send to anthropic +/* +https://docs.anthropic.com/en/docs/build-with-claude/tool-use#tool-use-examples +anthropic MESSAGE (role=assistant): +"content": [{ + "type": "text", + "text": "I need to call the get_weather function, and the user wants SF, which is likely San Francisco, CA." +}, { + "type": "tool_use", + "id": "toolu_01A09q90qw90lq917835lq9", + "name": "get_weather", + "input": { "location": "San Francisco, CA", "unit": "celsius" } +}] +anthropic RESPONSE (role=user): +"content": [{ + "type": "tool_result", + "tool_use_id": "toolu_01A09q90qw90lq917835lq9", + "content": "15 degrees" +}] + + +Converts: +assistant: ...content +tool: (id, name, params) +-> +assistant: ...content, call(name, id, params) +user: ...content, result(id, content) +*/ + + +const prepareMessages_anthropic_tools = (messages: SimpleLLMMessage[], supportsAnthropicReasoning: boolean): LLMChatMessage[] => { + const newMessages: (AnthropicLLMChatMessage | (SimpleLLMMessage & { role: 'tool' }))[] = messages; + + for (let i = 0; i < messages.length; i += 1) { + const currMsg = messages[i] + + // add anthropic reasoning + if (currMsg.role === 'assistant') { + if (currMsg.anthropicReasoning && supportsAnthropicReasoning) { + + const content = currMsg.content + newMessages[i] = { + role: 'assistant', + content: content ? [...currMsg.anthropicReasoning, { type: 'text' as const, text: content }] : currMsg.anthropicReasoning + } + } + continue + } + + if (currMsg.role === 'user') { + newMessages[i] = { + role: 'user', + content: currMsg.content, + } + continue + } + + if (currMsg.role === 'tool') { + // add anthropic tools + const prevMsg = 0 <= i - 1 && i - 1 <= newMessages.length ? newMessages[i - 1] : undefined + + // make it so the assistant called the tool + if (prevMsg?.role === 'assistant') { + if (typeof prevMsg.content === 'string') prevMsg.content = [{ type: 'text', text: prevMsg.content }] + prevMsg.content.push({ type: 'tool_use', id: currMsg.id, name: currMsg.name, input: currMsg.rawParams }) + } + + // turn each tool into a user message with tool results at the end + newMessages[i] = { + role: 'user', + content: [{ type: 'tool_result', tool_use_id: currMsg.id, content: currMsg.content }] + } + continue + } + + } + + // we just removed the tools + return newMessages as AnthropicLLMChatMessage[] +} + + +const prepareMessages_XML_tools = (messages: SimpleLLMMessage[], supportsAnthropicReasoning: boolean): LLMChatMessage[] => { + + const llmChatMessages: LLMChatMessage[] = []; + for (let i = 0; i < messages.length; i += 1) { + + const c = messages[i] + const next = 0 <= i + 1 && i + 1 <= messages.length - 1 ? messages[i + 1] : null + + if (c.role === 'assistant') { + // if called a tool (message after it), re-add its XML to the message + // alternatively, could just hold onto the original output, but this way requires less piping raw strings everywhere + let content: LLMChatMessage['content'] = c.content + if (next?.role === 'tool') { + content = `${content}\n\n${toolCallXMLStr(next.name, next.rawParams)}` + } + + // anthropic reasoning + if (c.anthropicReasoning && supportsAnthropicReasoning) { + content = content ? [...c.anthropicReasoning, { type: 'text' as const, text: content }] : c.anthropicReasoning + } + llmChatMessages.push({ + role: 'assistant', + content + }) + } + // add user or tool to the previous user message + else if (c.role === 'user' || c.role === 'tool') { + if (c.role === 'tool') + c.content = `<${c.name}_result>\n${c.content}\n` + + if (llmChatMessages.length === 0 || llmChatMessages[llmChatMessages.length - 1].role !== 'user') + llmChatMessages.push({ + role: 'user', + content: c.content + }) + else + llmChatMessages[llmChatMessages.length - 1].content += '\n\n' + c.content + } + } + return llmChatMessages +} + + + +const prepareMessages_providerSpecific = (messages: SimpleLLMMessage[], specialToolFormat: 'openai-style' | 'anthropic-style' | undefined, supportsAnthropicReasoning: boolean): LLMChatMessage[] => { + const llmChatMessages: LLMChatMessage[] = [] + if (!specialToolFormat) { // XML tool behavior + return prepareMessages_XML_tools(messages, supportsAnthropicReasoning) + } + else if (specialToolFormat === 'anthropic-style') { + return prepareMessages_anthropic_tools(messages, supportsAnthropicReasoning) + } + else if (specialToolFormat === 'openai-style') { + return prepareMessages_openai_tools(messages) + } + return llmChatMessages +} + + +// --- CHAT --- + +const prepareMessages = ({ + messages, + systemMessage, + aiInstructions, + supportsSystemMessage, + specialToolFormat, + supportsAnthropicReasoning, + contextWindow, + maxOutputTokens, +}: { + messages: SimpleLLMMessage[], + systemMessage: string, + aiInstructions: string, + supportsSystemMessage: false | 'system-role' | 'developer-role' | 'separated', + specialToolFormat: 'openai-style' | 'anthropic-style' | undefined, + supportsAnthropicReasoning: boolean, + contextWindow: number, + maxOutputTokens: number | null | undefined, +}): { messages: LLMChatMessage[], separateSystemMessage: string | undefined } => { + maxOutputTokens = maxOutputTokens ?? 4_096 // default to 4096 + + // ================ trim ================ + + messages = deepClone(messages) + messages = messages.map(m => ({ ...m, content: m.role !== 'tool' ? m.content.trim() : m.content })) + + // ================ fit into context ================ + + // the higher the weight, the higher the desire to truncate - TRIM HIGHEST WEIGHT MESSAGES + const alreadyTrimmedIdxes = new Set() + const weight = (message: SimpleLLMMessage, messages: SimpleLLMMessage[], idx: number) => { + const base = message.content.length + + let multiplier: number + multiplier = 1 + (messages.length - 1 - idx) / messages.length // slow rampdown from 2 to 1 as index increases + if (message.role === 'user') { + multiplier *= 1 + } + else { + multiplier *= 10 // llm tokens are far less valuable than user tokens + } + // 1st message, last 3 msgs, any already modified message should be low in weight + if (idx === 0 || idx >= messages.length - 1 - 3 || alreadyTrimmedIdxes.has(idx)) { + multiplier *= .05 + } + return base * multiplier + } + + const _findLargestByWeight = (messages: SimpleLLMMessage[]) => { + let largestIndex = -1 + let largestWeight = -Infinity + for (let i = 0; i < messages.length; i += 1) { + const m = messages[i] + const w = weight(m, messages, i) + if (w > largestWeight) { + largestWeight = w + largestIndex = i + } + } + return largestIndex + } + + let totalLen = 0 + for (const m of messages) { totalLen += m.content.length } + const charsNeedToTrim = totalLen - (contextWindow - maxOutputTokens) * CHARS_PER_TOKEN + + // <-----------------------------------------> + // 0 | | | + // | contextWindow | + // contextWindow - maxOut|putTokens + // totalLen + let remainingCharsToTrim = charsNeedToTrim + let i = 0 + + while (remainingCharsToTrim > 0) { + i += 1 + if (i > 100) break + + const trimIdx = _findLargestByWeight(messages) + const m = messages[trimIdx] + + // if can finish here, do + const numCharsWillTrim = m.content.length - TRIM_TO_LEN + if (numCharsWillTrim > remainingCharsToTrim) { + m.content = m.content.slice(0, m.content.length - remainingCharsToTrim).trim() + break + } + + remainingCharsToTrim -= numCharsWillTrim + m.content = m.content.substring(0, TRIM_TO_LEN - 3) + '...' + alreadyTrimmedIdxes.add(trimIdx) + } + + // ================ tools and anthropicReasoning ================ + const llmMessages: LLMChatMessage[] = prepareMessages_providerSpecific(messages, specialToolFormat, supportsAnthropicReasoning) + + // ================ system message concat ================ + + // find system messages and concatenate them + const newSystemMessage = aiInstructions ? + `${(systemMessage ? `${systemMessage}\n\n` : '')}GUIDELINES\n${aiInstructions}` + : systemMessage + + let separateSystemMessageStr: string | undefined = undefined + + // if supports system message + if (supportsSystemMessage) { + if (supportsSystemMessage === 'separated') + separateSystemMessageStr = newSystemMessage + else if (supportsSystemMessage === 'system-role') + llmMessages.unshift({ role: 'system', content: newSystemMessage }) // add new first message + else if (supportsSystemMessage === 'developer-role') + llmMessages.unshift({ role: 'developer', content: newSystemMessage }) // add new first message + } + // if does not support system message + else { + const newFirstMessage = { + role: 'user', + content: `\n${newSystemMessage}\n\n${llmMessages[0].content}` + } as const + llmMessages.splice(0, 1) // delete first message + llmMessages.unshift(newFirstMessage) // add new first message + } + + + // ================ no empty message ================ + for (const currMsg of llmMessages) { + if (currMsg.role === 'tool') continue + + // if content is a string, replace string with empty msg + if (typeof currMsg.content === 'string') + currMsg.content = currMsg.content || EMPTY_MESSAGE + else { + // if content is an array, replace any empty text entries with empty msg, and make sure there's at least 1 entry + for (const c of currMsg.content) { + if (c.type === 'text') c.text = c.text || EMPTY_MESSAGE + } + if (currMsg.content.length === 0) currMsg.content = [{ type: 'text', text: EMPTY_MESSAGE }] + } + } + + return { + messages: llmMessages, + separateSystemMessage: separateSystemMessageStr, + } as const +} + + + + + + + + + + + +export interface IConvertToLLMMessageService { + readonly _serviceBrand: undefined; + prepareLLMSimpleMessages: (opts: { simpleMessages: SimpleLLMMessage[], systemMessage: string, modelSelection: ModelSelection | null, featureName: FeatureName }) => { messages: LLMChatMessage[], separateSystemMessage: string | undefined }; + prepareLLMChatMessages: (opts: { chatMessages: ChatMessage[], chatMode: ChatMode, modelSelection: ModelSelection | null }) => Promise<{ messages: LLMChatMessage[], separateSystemMessage: string | undefined }> + prepareFIMMessage(opts: { messages: LLMFIMMessage, aiInstructions: string, }): { prefix: string, suffix: string, stopTokens: string[] } + +} + +export const IConvertToLLMMessageService = createDecorator('ConvertToLLMMessageService'); + + +class ConvertToLLMMessageService extends Disposable implements IConvertToLLMMessageService { + _serviceBrand: undefined; + + constructor( + @IModelService private readonly modelService: IModelService, + @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, + @IEditorService private readonly editorService: IEditorService, + @IDirectoryStrService private readonly directoryStrService: IDirectoryStrService, + @ITerminalToolService private readonly terminalToolService: ITerminalToolService, + @IVoidSettingsService private readonly voidSettingsService: IVoidSettingsService, + ) { + super() + } + + + // system message + private _generateChatMessagesSystemMessage = async (chatMode: ChatMode, specialToolFormat: 'openai-style' | 'anthropic-style' | undefined) => { + const workspaceFolders = this.workspaceContextService.getWorkspace().folders.map(f => f.uri.fsPath) + + const openedURIs = this.modelService.getModels().filter(m => m.isAttachedToEditor()).map(m => m.uri.fsPath) || []; + const activeURI = this.editorService.activeEditor?.resource?.fsPath; + + const directoryStr = await this.directoryStrService.getAllDirectoriesStr({ + cutOffMessage: chatMode === 'agent' || chatMode === 'gather' ? + `...Directories string cut off, use tools to read more...` + : `...Directories string cut off, ask user for more if necessary...` + }) + const includeXMLToolDefinitions = specialToolFormat === undefined + + const runningTerminalIds = this.terminalToolService.listTerminalIds() + const systemMessage = chat_systemMessage({ workspaceFolders, openedURIs, directoryStr, activeURI, runningTerminalIds, chatMode, includeXMLToolDefinitions }) + return systemMessage + } + + + + + // --- LLM Chat messages --- + + private _chatMessagesToSimpleMessages(chatMessages: ChatMessage[]): SimpleLLMMessage[] { + const simpleLLMMessages: SimpleLLMMessage[] = [] + + for (const m of chatMessages) { + if (m.role === 'checkpoint') continue + if (m.role === 'interrupted_streaming_tool') continue + if (m.role === 'assistant') { + simpleLLMMessages.push({ + role: m.role, + content: m.displayContent, + anthropicReasoning: m.anthropicReasoning, + }) + } + else if (m.role === 'tool') { + simpleLLMMessages.push({ + role: m.role, + content: m.content, + name: m.name, + id: m.id, + rawParams: m.rawParams, + }) + } + else if (m.role === 'user') { + simpleLLMMessages.push({ + role: m.role, + content: m.content, + }) + } + } + return simpleLLMMessages + } + + prepareLLMSimpleMessages: IConvertToLLMMessageService['prepareLLMSimpleMessages'] = ({ simpleMessages, systemMessage, modelSelection, featureName }) => { + if (modelSelection === null) return { messages: [], separateSystemMessage: undefined } + const { providerName, modelName } = modelSelection + const { + specialToolFormat, + contextWindow, + supportsSystemMessage, + } = getModelCapabilities(providerName, modelName) + + const modelSelectionOptions = this.voidSettingsService.state.optionsOfModelSelection[featureName][modelSelection.providerName]?.[modelSelection.modelName] + const aiInstructions = this.voidSettingsService.state.globalSettings.aiInstructions + + const isReasoningEnabled = getIsReasoningEnabledState(featureName, providerName, modelName, modelSelectionOptions) + const maxOutputTokens = getMaxOutputTokens(providerName, modelName, { isReasoningEnabled }) + + const { messages, separateSystemMessage } = prepareMessages({ + messages: simpleMessages, + systemMessage, + aiInstructions, + supportsSystemMessage, + specialToolFormat, + supportsAnthropicReasoning: providerName === 'anthropic', + contextWindow, + maxOutputTokens, + }) + return { messages, separateSystemMessage }; + + } + prepareLLMChatMessages: IConvertToLLMMessageService['prepareLLMChatMessages'] = async ({ chatMessages, chatMode, modelSelection }) => { + if (modelSelection === null) return { messages: [], separateSystemMessage: undefined } + const { providerName, modelName } = modelSelection + const { + specialToolFormat, + contextWindow, + supportsSystemMessage, + } = getModelCapabilities(providerName, modelName) + const systemMessage = await this._generateChatMessagesSystemMessage(chatMode, specialToolFormat) + + const modelSelectionOptions = this.voidSettingsService.state.optionsOfModelSelection['Chat'][modelSelection.providerName]?.[modelSelection.modelName] + const aiInstructions = this.voidSettingsService.state.globalSettings.aiInstructions + + const isReasoningEnabled = getIsReasoningEnabledState('Chat', providerName, modelName, modelSelectionOptions) + const maxOutputTokens = getMaxOutputTokens(providerName, modelName, { isReasoningEnabled }) + const llmMessages = this._chatMessagesToSimpleMessages(chatMessages) + + const { messages, separateSystemMessage } = prepareMessages({ + messages: llmMessages, + systemMessage, + aiInstructions, + supportsSystemMessage, + specialToolFormat, + supportsAnthropicReasoning: providerName === 'anthropic', + contextWindow, + maxOutputTokens, + }) + return { messages, separateSystemMessage }; + + } + + + // --- FIM --- + + prepareFIMMessage: IConvertToLLMMessageService['prepareFIMMessage'] = ({ messages, aiInstructions }) => { + + let prefix = `\ +${!aiInstructions ? '' : `\ +// Instructions: +// Do not output an explanation. Try to avoid outputting comments. Only output the middle code. +${aiInstructions.split('\n').map(line => `//${line}`).join('\n')}`} + +${messages.prefix}` + + const suffix = messages.suffix + const stopTokens = messages.stopTokens + return { prefix, suffix, stopTokens } + } + + +} + + +// pick one and delete the other: +registerSingleton(IConvertToLLMMessageService, ConvertToLLMMessageService, InstantiationType.Eager); + + + + + + + + +/* +Gemini has this, but they're openai-compat so we don't need to implement this +gemini request: +{ "role": "assistant", + "content": null, + "function_call": { + "name": "get_weather", + "arguments": { + "latitude": 48.8566, + "longitude": 2.3522 + } + } +} + +gemini response: +{ "role": "assistant", + "function_response": { + "name": "get_weather", + "response": { + "temperature": "15°C", + "condition": "Cloudy" + } + } +} +*/ + + + diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index 3f15cc21..6e480048 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -47,6 +47,7 @@ import { IVoidModelService } from '../common/voidModelService.js'; import { deepClone } from '../../../../base/common/objects.js'; import { acceptBg, acceptBorder, buttonFontSize, buttonTextColor, rejectBg, rejectBorder } from '../common/helpers/colors.js'; import { DiffArea, Diff, CtrlKZone, VoidFileSnapshot, DiffAreaSnapshotEntry, diffAreaSnapshotKeys, DiffZone, TrackingZone, ComputedDiff } from '../common/editCodeServiceTypes.js'; +import { IConvertToLLMMessageService } from './convertToLLMMessageService.js'; const configOfBG = (color: Color) => { return { dark: color, light: color, hcDark: color, hcLight: color, } @@ -202,6 +203,7 @@ class EditCodeService extends Disposable implements IEditCodeService { @IVoidSettingsService private readonly _settingsService: IVoidSettingsService, // @IFileService private readonly _fileService: IFileService, @IVoidModelService private readonly _voidModelService: IVoidModelService, + @IConvertToLLMMessageService private readonly _convertToLLMMessageService: IConvertToLLMMessageService, ) { super(); @@ -1267,6 +1269,10 @@ class EditCodeService extends Disposable implements IEditCodeService { private _initializeWriteoverStream(opts: StartApplyingOpts): [DiffZone, Promise] | undefined { const { from, } = opts + const featureName: FeatureName = opts.from === 'ClickApply' ? 'Apply' : 'Ctrl+K' + const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName] + const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[featureName][modelSelection.providerName]?.[modelSelection.modelName] : undefined + const uri = this._getURIBeforeStartApplying(opts) if (!uri) return @@ -1300,12 +1306,16 @@ class EditCodeService extends Disposable implements IEditCodeService { const originalCode = startRange === 'fullFile' ? originalFileCode : originalFileCode.split('\n').slice((startRange[0] - 1), (startRange[1] - 1) + 1).join('\n') const language = model.getLanguageId() let messages: LLMChatMessage[] + let separateSystemMessage: string | undefined if (from === 'ClickApply') { - const userContent = rewriteCode_userMessage({ originalCode, applyStr: opts.applyStr, language }) - messages = [ - { role: 'system', content: rewriteCode_systemMessage, }, - { role: 'user', content: userContent, } - ] + const { messages: a, separateSystemMessage: b } = this._convertToLLMMessageService.prepareLLMSimpleMessages({ + systemMessage: rewriteCode_systemMessage, + simpleMessages: [{ role: 'user', content: rewriteCode_userMessage({ originalCode, applyStr: opts.applyStr, language }), }], + featureName, + modelSelection, + }) + messages = a + separateSystemMessage = b } else if (from === 'QuickEdit') { if (!ctrlKZoneIfQuickEdit) return @@ -1316,11 +1326,16 @@ class EditCodeService extends Disposable implements IEditCodeService { const endLine = startRange === 'fullFile' ? model.getLineCount() : startRange[1] const { prefix, suffix } = voidPrefixAndSuffix({ fullFileStr: originalFileCode, startLine, endLine }) const userContent = ctrlKStream_userMessage({ selection: originalCode, instructions: instructions, prefix, suffix, fimTags: quickEditFIMTags, language }) - // type: 'messages', - messages = [ - { role: 'system', content: ctrlKStream_systemMessage({ quickEditFIMTags: quickEditFIMTags }), }, - { role: 'user', content: userContent, } - ] + + const { messages: a, separateSystemMessage: b } = this._convertToLLMMessageService.prepareLLMSimpleMessages({ + systemMessage: ctrlKStream_systemMessage({ quickEditFIMTags: quickEditFIMTags }), + simpleMessages: [{ role: 'user', content: userContent, }], + featureName, + modelSelection, + }) + messages = a + separateSystemMessage = b + } else { throw new Error(`featureName ${from} is invalid`) } @@ -1384,10 +1399,6 @@ class EditCodeService extends Disposable implements IEditCodeService { const latestStreamLocationMutable: StreamLocationMutable = { line: diffZone.startLine, addedSplitYet: false, col: 1, originalCodeStartLine: 1 } - const featureName: FeatureName = opts.from === 'ClickApply' ? 'Apply' : 'Ctrl+K' - const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName] - const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[featureName][modelSelection.providerName]?.[modelSelection.modelName] : undefined - // allowed to throw errors - this is called inside a promise that handles everything const runWriteover = async () => { let shouldSendAnotherMessage = true @@ -1410,6 +1421,7 @@ class EditCodeService extends Disposable implements IEditCodeService { messages, modelSelection, modelSelectionOptions, + separateSystemMessage, chatMode: null, // not chat onText: (params) => { const { fullText: fullText_ } = params @@ -1485,6 +1497,9 @@ class EditCodeService extends Disposable implements IEditCodeService { private _initializeSearchAndReplaceStream(opts: StartApplyingOpts & { from: 'ClickApply' }): [DiffZone, Promise] | undefined { const { from, applyStr, } = opts + const featureName: FeatureName = 'Apply' + const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName] + const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[featureName][modelSelection.providerName]?.[modelSelection.modelName] : undefined const uri = this._getURIBeforeStartApplying(opts) if (!uri) return @@ -1498,10 +1513,13 @@ class EditCodeService extends Disposable implements IEditCodeService { // build messages - ask LLM to generate search/replace block text const originalFileCode = model.getValue(EndOfLinePreference.LF) const userMessageContent = searchReplace_userMessage({ originalCode: originalFileCode, applyStr: applyStr }) - const messages: LLMChatMessage[] = [ - { role: 'system', content: searchReplace_systemMessage }, - { role: 'user', content: userMessageContent }, - ] + + const { messages, separateSystemMessage: separateSystemMessage } = this._convertToLLMMessageService.prepareLLMSimpleMessages({ + systemMessage: searchReplace_systemMessage, + simpleMessages: [{ role: 'user', content: userMessageContent, }], + featureName, + modelSelection, + }) // if URI is already streaming, return (should never happen, caller is responsible for checking) if (this._uriIsStreaming(uri)) return @@ -1593,10 +1611,6 @@ class EditCodeService extends Disposable implements IEditCodeService { const addedTrackingZoneOfBlockNum: TrackingZone[] = [] diffZone._streamState.line = 1 - const featureName: FeatureName = 'Apply' - const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName] - const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[featureName][modelSelection.providerName]?.[modelSelection.modelName] : undefined - const N_RETRIES = 2 // allowed to throw errors - this is called inside a promise that handles everything @@ -1628,6 +1642,7 @@ class EditCodeService extends Disposable implements IEditCodeService { messages, modelSelection, modelSelectionOptions, + separateSystemMessage, chatMode: null, // not chat onText: (params) => { const { fullText } = params @@ -1682,7 +1697,7 @@ class EditCodeService extends Disposable implements IEditCodeService { console.log('---------') const content = errContentOfInvalidStr(originalBounds, block.orig, blockNum, blocks) messages.push( - { role: 'assistant', content: fullText, anthropicReasoning: null }, // latest output + { role: 'assistant', content: fullText }, // 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 78e23395..5f4193f0 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 @@ -2492,7 +2492,6 @@ export const SidebarChat = () => { role: 'assistant', displayContent: displayContentSoFar ?? '', reasoning: reasoningSoFar ?? '', - toolCall: toolCallSoFar, anthropicReasoning: null, }} messageIdx={streamingChatIdx} diff --git a/src/vs/workbench/contrib/void/browser/searchReplaceCacheService.ts b/src/vs/workbench/contrib/void/browser/searchReplaceCacheService.ts deleted file mode 100644 index 4fbf283e..00000000 --- a/src/vs/workbench/contrib/void/browser/searchReplaceCacheService.ts +++ /dev/null @@ -1,44 +0,0 @@ -/*-------------------------------------------------------------------------------------- - * Copyright 2025 Glass Devtools, Inc. All rights reserved. - * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. - *--------------------------------------------------------------------------------------*/ - -import { Emitter, Event } from '../../../../base/common/event.js'; -import { Disposable } from '../../../../base/common/lifecycle.js'; -import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; -import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; - - - -export interface ISearchReplaceService { - readonly _serviceBrand: undefined; -} - -export const ISearchReplaceService = createDecorator('SearchReplaceCacheService'); -export class SearchReplaceService extends Disposable implements ISearchReplaceService { - _serviceBrand: undefined; - - private readonly _onDidChangeState = new Emitter(); - readonly onDidChangeState: Event = this._onDidChangeState.event; - - constructor( - // @ILLMMessageService private readonly llmMessageService: ILLMMessageService, - ) { - super() - } - - // send(params: ServiceSendLLMMessageParams & { onText: (p: { newText: string, fullText: string }) => { retry: boolean } }) { - // this.llmMessageService.sendLLMMessage({ - // ...params as ServiceSendLLMMessageParams, - // onText: (p) => { - // const { retry } = params.onText(p) - // if (retry) { - - // } - // } - // }) - // } - -} - -registerSingleton(ISearchReplaceService, SearchReplaceService, InstantiationType.Eager); diff --git a/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts b/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts index 18df3709..fbf8e18e 100644 --- a/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts +++ b/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts @@ -6,15 +6,17 @@ import { URI } from '../../../../base/common/uri.js'; import { VoidFileSnapshot } from './editCodeServiceTypes.js'; import { ToolName } from './prompt/prompts.js'; -import { AnthropicReasoning, RawToolCallObj } from './sendLLMMessageTypes.js'; +import { AnthropicReasoning, RawToolParamsObj } from './sendLLMMessageTypes.js'; import { ToolCallParams, ToolResultType } from './toolsServiceTypes.js'; export type ToolMessage = { role: 'tool'; content: string; // give this result to LLM (string of value) + id: string; + rawParams: RawToolParamsObj; } & ( // in order of events: - | { type: 'invalid_params', result: null, name: T, params: RawToolCallObj | null, } + | { type: 'invalid_params', result: null, name: T, } | { type: 'tool_request', result: null, name: T, params: ToolCallParams[T], } // params were validated, awaiting user @@ -22,7 +24,7 @@ export type ToolMessage = { | { type: 'tool_error', result: string, name: T, params: ToolCallParams[T], } // error when tool was running | { type: 'success', result: Awaited, name: T, params: ToolCallParams[T], } - | { type: 'rejected', result: null, name: T, params: ToolCallParams[T], } + | { type: 'rejected', result: null, name: T, params: ToolCallParams[T] } ) // user rejected export type DecorativeCanceledTool = { @@ -58,7 +60,6 @@ export type ChatMessage = role: 'assistant'; displayContent: 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 - toolCall: RawToolCallObj | undefined; anthropicReasoning: AnthropicReasoning[] | null; // anthropic reasoning } diff --git a/src/vs/workbench/contrib/void/common/modelCapabilities.ts b/src/vs/workbench/contrib/void/common/modelCapabilities.ts index 6be9b608..5d5b4169 100644 --- a/src/vs/workbench/contrib/void/common/modelCapabilities.ts +++ b/src/vs/workbench/contrib/void/common/modelCapabilities.ts @@ -130,6 +130,7 @@ export type VoidStaticModelInfo = { // not stateful } supportsSystemMessage: false | 'system-role' | 'developer-role' | 'separated'; // separated = anthropic where "system" is a special paramete + specialToolFormat?: 'openai-style' | 'anthropic-style', // null defaults to XML supportsFIM: boolean; reasoningCapabilities: false | { @@ -377,6 +378,7 @@ const anthropicModelOptions = { cost: { input: 3.00, cache_read: 0.30, cache_write: 3.75, output: 15.00 }, downloadable: false, supportsFIM: false, + specialToolFormat: 'anthropic-style', supportsSystemMessage: 'separated', reasoningCapabilities: { supportsReasoning: true, @@ -385,6 +387,7 @@ const anthropicModelOptions = { reasoningMaxOutputTokens: 64_000, // can bump it to 128_000 with beta mode output-128k-2025-02-19 reasoningBudgetSlider: { type: 'slider', min: 1024, max: 32_000, default: 1024 }, // they recommend batching if max > 32_000 }, + }, 'claude-3-5-sonnet-20241022': { contextWindow: 200_000, @@ -392,6 +395,7 @@ const anthropicModelOptions = { cost: { input: 3.00, cache_read: 0.30, cache_write: 3.75, output: 15.00 }, downloadable: false, supportsFIM: false, + specialToolFormat: 'anthropic-style', supportsSystemMessage: 'separated', reasoningCapabilities: false, }, @@ -401,6 +405,7 @@ const anthropicModelOptions = { cost: { input: 0.80, cache_read: 0.08, cache_write: 1.00, output: 4.00 }, downloadable: false, supportsFIM: false, + specialToolFormat: 'anthropic-style', supportsSystemMessage: 'separated', reasoningCapabilities: false, }, @@ -410,6 +415,7 @@ const anthropicModelOptions = { cost: { input: 15.00, cache_read: 1.50, cache_write: 18.75, output: 75.00 }, downloadable: false, supportsFIM: false, + specialToolFormat: 'anthropic-style', supportsSystemMessage: 'separated', reasoningCapabilities: false, }, @@ -418,6 +424,7 @@ const anthropicModelOptions = { downloadable: false, maxOutputTokens: 4_096, supportsFIM: false, + specialToolFormat: 'anthropic-style', supportsSystemMessage: 'separated', reasoningCapabilities: false, } @@ -457,6 +464,7 @@ const openAIModelOptions = { // https://platform.openai.com/docs/pricing cost: { input: 2.00, output: 8.00, cache_read: 0.50 }, downloadable: false, supportsFIM: false, + specialToolFormat: 'openai-style', supportsSystemMessage: 'developer-role', reasoningCapabilities: false, }, @@ -466,6 +474,7 @@ const openAIModelOptions = { // https://platform.openai.com/docs/pricing cost: { input: 0.40, output: 1.60, cache_read: 0.10 }, downloadable: false, supportsFIM: false, + specialToolFormat: 'openai-style', supportsSystemMessage: 'developer-role', reasoningCapabilities: false, }, @@ -475,6 +484,7 @@ const openAIModelOptions = { // https://platform.openai.com/docs/pricing cost: { input: 0.10, output: 0.40, cache_read: 0.03 }, downloadable: false, supportsFIM: false, + specialToolFormat: 'openai-style', supportsSystemMessage: 'developer-role', reasoningCapabilities: false, }, @@ -502,6 +512,7 @@ const openAIModelOptions = { // https://platform.openai.com/docs/pricing cost: { input: 2.50, cache_read: 1.25, output: 10.00, }, downloadable: false, supportsFIM: false, + specialToolFormat: 'openai-style', supportsSystemMessage: 'system-role', reasoningCapabilities: false, }, @@ -520,6 +531,7 @@ const openAIModelOptions = { // https://platform.openai.com/docs/pricing cost: { input: 0.15, cache_read: 0.075, output: 0.60, }, downloadable: false, supportsFIM: false, + specialToolFormat: 'openai-style', supportsSystemMessage: 'system-role', // ?? reasoningCapabilities: false, }, @@ -550,6 +562,15 @@ const xAIModelOptions = { supportsSystemMessage: 'system-role', reasoningCapabilities: false, }, + // 'grok-3': { + // contextWindow: 1_000_000, + // maxOutputTokens: null, + // cost: {}, + // downloadable: false, + // supportsFIM: false, + // supportsSystemMessage: 'system-role', + // reasoningCapabilities: {canIOReasoning:false, canTurnOffReasoning:true,}, + // } } as const satisfies { [s: string]: VoidStaticModelInfo } const xAISettings: VoidStaticProviderInfo = { @@ -1032,6 +1053,14 @@ export const getIsReasoningEnabledState = ( } +export const getMaxOutputTokens = (providerName: ProviderName, modelName: string, opts: { isReasoningEnabled: boolean }) => { + const { + reasoningCapabilities, + maxOutputTokens + } = getModelCapabilities(providerName, modelName) + return opts.isReasoningEnabled && reasoningCapabilities ? reasoningCapabilities.reasoningMaxOutputTokens : maxOutputTokens +} + // used to force reasoning state (complex) into something simple we can just read from when sending a message export const getSendableReasoningInfo = ( featureName: FeatureName, diff --git a/src/vs/workbench/contrib/void/common/prompt/prompts.ts b/src/vs/workbench/contrib/void/common/prompt/prompts.ts index 03264665..04b809cf 100644 --- a/src/vs/workbench/contrib/void/common/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/common/prompt/prompts.ts @@ -6,7 +6,7 @@ import { EndOfLinePreference } from '../../../../../editor/common/model.js'; import { StagingSelectionItem } from '../chatThreadServiceTypes.js'; import { os } from '../helpers/systemInfo.js'; -import { RawToolCallObj } from '../sendLLMMessageTypes.js'; +import { RawToolParamsObj } from '../sendLLMMessageTypes.js'; import { toolNamesThatRequireApproval } from '../toolsServiceTypes.js'; import { IVoidModelService } from '../voidModelService.js'; import { ChatMode } from '../voidSettingsTypes.js'; @@ -218,12 +218,11 @@ Format: }).join('\n\n')}` } -export const toolCallXMLStr = (toolCall: RawToolCallObj) => { - const t = toolCall - const params = Object.keys(t.rawParams).map(paramName => `<${paramName}>${t.rawParams[paramName as ToolParamName]}`).join('\n') +export const toolCallXMLStr = (toolName: ToolName, toolParams: RawToolParamsObj) => { + const params = Object.keys(toolParams).map(paramName => `<${paramName}>${toolParams[paramName as ToolParamName]}`).join('\n') return `\ -<${toolCall.name}>${!params ? '' : `\n${params}`} -` +<${toolName}>${!params ? '' : `\n${params}`} +` .replace('\t', ' ') } @@ -231,7 +230,7 @@ export const toolCallXMLStr = (toolCall: RawToolCallObj) => { // - You are allowed to call multiple tools by specifying them consecutively. However, there should be NO text or writing between tool calls or after them. const systemToolsXMLPrompt = (chatMode: ChatMode) => { const tools = availableTools(chatMode) - if (!tools || tools.length === 0) return '' + if (!tools || tools.length === 0) return null const toolXMLDefinitions = (`\ Available tools: @@ -255,7 +254,7 @@ ${toolCallXMLGuidelines}` // ======================================================== chat (normal, gather, agent) ======================================================== -export const chat_systemMessage = ({ workspaceFolders, openedURIs, activeURI, runningTerminalIds, directoryStr, chatMode: mode }: { workspaceFolders: string[], directoryStr: string, openedURIs: string[], activeURI: string | undefined, runningTerminalIds: string[], chatMode: ChatMode }) => { +export const chat_systemMessage = ({ workspaceFolders, openedURIs, activeURI, runningTerminalIds, directoryStr, chatMode: mode, includeXMLToolDefinitions }: { workspaceFolders: string[], directoryStr: string, openedURIs: string[], activeURI: string | undefined, runningTerminalIds: string[], chatMode: ChatMode, includeXMLToolDefinitions: boolean }) => { const header = (`You are an expert coding ${mode === 'agent' ? 'agent' : 'assistant'} whose job is \ ${mode === 'agent' ? `to help the user develop, run, and make changes to their codebase.` : mode === 'gather' ? `to search, understand, and reference files in the user's codebase.` @@ -289,7 +288,7 @@ ${directoryStr} `) - const toolDefinitions = systemToolsXMLPrompt(mode) + const toolDefinitions = includeXMLToolDefinitions ? systemToolsXMLPrompt(mode) : null const details: string[] = [] diff --git a/src/vs/workbench/contrib/void/common/sendLLMMessageService.ts b/src/vs/workbench/contrib/void/common/sendLLMMessageService.ts index 5b3023ce..e6a30360 100644 --- a/src/vs/workbench/contrib/void/common/sendLLMMessageService.ts +++ b/src/vs/workbench/contrib/void/common/sendLLMMessageService.ts @@ -13,7 +13,6 @@ import { generateUuid } from '../../../../base/common/uuid.js'; import { Event } from '../../../../base/common/event.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; import { IVoidSettingsService } from './voidSettingsService.js'; -// import { INotificationService } from '../../notification/common/notification.js'; // calls channel to implement features export const ILLMMessageService = createDecorator('llmMessageService'); @@ -98,6 +97,7 @@ export class LLMMessageService extends Disposable implements ILLMMessageService return null } + const { settingsOfProvider, } = this.voidSettingsService.state // add state for request id const requestId = generateUuid(); @@ -106,13 +106,9 @@ export class LLMMessageService extends Disposable implements ILLMMessageService this.llmMessageHooks.onError[requestId] = onError this.llmMessageHooks.onAbort[requestId] = onAbort // used internally only - const { aiInstructions } = this.voidSettingsService.state.globalSettings - const { settingsOfProvider, } = this.voidSettingsService.state - // params will be stripped of all its functions over the IPC channel this.channel.call('sendLLMMessage', { ...proxyParams, - aiInstructions, requestId, settingsOfProvider, modelSelection, diff --git a/src/vs/workbench/contrib/void/common/sendLLMMessageTypes.ts b/src/vs/workbench/contrib/void/common/sendLLMMessageTypes.ts index 26167aaf..a345fde0 100644 --- a/src/vs/workbench/contrib/void/common/sendLLMMessageTypes.ts +++ b/src/vs/workbench/contrib/void/common/sendLLMMessageTypes.ts @@ -27,16 +27,39 @@ export const getErrorMessage: (error: unknown) => string = (error) => { } -export type LLMChatMessage = { - role: 'system'; - content: string; + +export type AnthropicLLMChatMessage = { + role: 'assistant', + content: string | (AnthropicReasoning | { type: 'text'; text: string } + | { type: 'tool_use'; name: string; input: Record; id: string; } + )[]; } | { - role: 'user'; + role: 'user', + content: string | ( + { type: 'text'; text: string; } | { type: 'tool_result'; tool_use_id: string; content: string; } + )[] +} +export type OpenAILLMChatMessage = { + role: 'system' | 'user' | 'developer'; content: string; } | { role: 'assistant', - content: string; // text content - anthropicReasoning: AnthropicReasoning[] | null; + content: string | (AnthropicReasoning | { type: 'text'; text: string })[]; + tool_calls?: { type: 'function'; id: string; function: { name: string; arguments: string; } }[]; +} | { + role: 'tool', + content: string; + tool_call_id: string; +} +export type LLMChatMessage = AnthropicLLMChatMessage | OpenAILLMChatMessage + + + + +export type LLMFIMMessage = { + prefix: string; + suffix: string; + stopTokens: string[]; } @@ -47,10 +70,10 @@ export type RawToolCallObj = { name: ToolName; rawParams: RawToolParamsObj; doneParams: ToolParamName[]; + id: string; isDone: boolean; }; - export type AnthropicReasoning = ({ type: 'thinking'; thinking: any; signature: string; } | { type: 'redacted_thinking', data: any }) export type OnText = (p: { fullText: string; fullReasoning: string; toolCall?: RawToolCallObj }) => void @@ -60,23 +83,18 @@ export type OnAbort = () => void export type AbortRef = { current: (() => void) | null } -export type LLMFIMMessage = { - prefix: string; - suffix: string; - stopTokens: string[]; -} - +// service types type SendLLMType = { messagesType: 'chatMessages'; - messages: LLMChatMessage[]; + messages: LLMChatMessage[]; // the type of raw chat messages that we send to Anthropic, OAI, etc + separateSystemMessage: string | undefined; chatMode: ChatMode | null; } | { messagesType: 'FIMMessage'; messages: LLMFIMMessage; + separateSystemMessage?: undefined; chatMode?: undefined; } - -// service types export type ServiceSendLLMMessageParams = { onText: OnText; onFinalMessage: OnFinalMessage; @@ -95,8 +113,6 @@ export type SendLLMMessageParams = { logging: { loggingName: string, loggingExtras?: { [k: string]: any } }; abortRef: AbortRef; - aiInstructions: string; - modelSelection: ModelSelection; modelSelectionOptions: ModelSelectionOptions | undefined; 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 463d0c06..ed3e20fa 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/extractGrammar.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/extractGrammar.ts @@ -3,6 +3,7 @@ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. *--------------------------------------------------------------------------------------*/ +import { generateUuid } from '../../../../../base/common/uuid.js' import { endsWithAnyPrefixOf, SurroundingsRemover } from '../../common/helpers/extractCodeFromResult.js' import { availableTools, InternalToolInfo, ToolName, ToolParamName } from '../../common/prompt/prompts.js' import { OnFinalMessage, OnText, RawToolCallObj, RawToolParamsObj } from '../../common/sendLLMMessageTypes.js' @@ -160,7 +161,7 @@ const findIndexOfAny = (fullText: string, matches: string[]) => { type ToolOfToolName = { [toolName: string]: InternalToolInfo | undefined } -const parseXMLPrefixToToolCall = (toolName: ToolName, str: string, toolOfToolName: ToolOfToolName): RawToolCallObj => { +const parseXMLPrefixToToolCall = (toolName: ToolName, toolId: string, str: string, toolOfToolName: ToolOfToolName): RawToolCallObj => { const paramsObj: RawToolParamsObj = {} const doneParams: ToolParamName[] = [] let isDone = false @@ -179,7 +180,8 @@ const parseXMLPrefixToToolCall = (toolName: ToolName, str: string, toolOfToolNam name: toolName, rawParams: paramsObj, doneParams: doneParams, - isDone: isDone + isDone: isDone, + id: toolId, } return ans } @@ -255,8 +257,10 @@ const parseXMLPrefixToToolCall = (toolName: ToolName, str: string, toolOfToolNam } export const extractToolsWrapper = ( - onText: OnText, onFinalMessage: OnFinalMessage, chatMode: ChatMode + onText: OnText, onFinalMessage: OnFinalMessage, chatMode: ChatMode | null ): { newOnText: OnText, newOnFinalMessage: OnFinalMessage } => { + + if (!chatMode) return { newOnText: onText, newOnFinalMessage: onFinalMessage } const tools = availableTools(chatMode) if (!tools) return { newOnText: onText, newOnFinalMessage: onFinalMessage } @@ -264,6 +268,8 @@ export const extractToolsWrapper = ( const toolOpenTags = tools.map(t => `<${t.name}>`) for (const t of tools) { toolOfToolName[t.name] = t } + const toolId = generateUuid() + // detect , etc let fullText = ''; let trueFullText = '' @@ -315,14 +321,12 @@ export const extractToolsWrapper = ( if (foundOpenTag !== null) { latestToolCall = parseXMLPrefixToToolCall( foundOpenTag.toolName, + toolId, trueFullText.substring(foundOpenTag.idx, Infinity), toolOfToolName, ) - } - - onText({ ...params, fullText, diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts deleted file mode 100644 index 4f1d3661..00000000 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts +++ /dev/null @@ -1,524 +0,0 @@ -/*-------------------------------------------------------------------------------------- - * Copyright 2025 Glass Devtools, Inc. All rights reserved. - * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. - *--------------------------------------------------------------------------------------*/ - -import { AnthropicReasoning, LLMChatMessage, LLMFIMMessage } from '../../common/sendLLMMessageTypes.js'; -import { deepClone } from '../../../../../base/common/objects.js'; - - -export const parseObject = (args: unknown) => { - if (typeof args === 'object') - return args - if (typeof args === 'string') - try { return JSON.parse(args) } - catch (e) { return { args } } - return {} -} - - -type InternalLLMChatMessage = { - role: 'system' | 'user'; - content: string; -} | { - role: 'assistant', - content: string | (AnthropicReasoning | { type: 'text'; text: string })[]; -} - - -const EMPTY_MESSAGE = '(empty message)' - -const prepareMessages_normalize = ({ messages: messages_ }: { messages: LLMChatMessage[] }): { messages: LLMChatMessage[] } => { - const messages = deepClone(messages_) - const newMessages: LLMChatMessage[] = [] - if (messages.length >= 0) newMessages.push(messages[0]) - - // remove duplicate roles - we used to do this, but we don't anymore - for (let i = 1; i < messages.length; i += 1) { - const m = messages[i] - newMessages.push(m) - } - const finalMessages = newMessages.map(m => ({ ...m, content: m.content.trim() })) - return { messages: finalMessages } -} - - - - - - -const CHARS_PER_TOKEN = 4 -const TRIM_TO_LEN = 60 - -const prepareMessages_fitIntoContext = ({ messages, contextWindow, maxOutputTokens }: { messages: LLMChatMessage[], contextWindow: number, maxOutputTokens: number }): { messages: LLMChatMessage[] } => { - - // the higher the weight, the higher the desire to truncate - const alreadyTrimmedIdxes = new Set() - const weight = (message: LLMChatMessage, messages: LLMChatMessage[], idx: number) => { - const base = message.content.length - - let multiplier: number - if (message.role === 'system') - return 0 // never erase system message - - multiplier = 1 + (messages.length - 1 - idx) / messages.length // slow rampdown from 2 to 1 as index increases - if (message.role === 'user') { - multiplier *= 1 - } - else { - multiplier *= 10 // llm tokens are far less valuable than user tokens - } - - // 1st message, last 3 msgs, any already modified message should be low in weight - if (idx === 0 || idx >= messages.length - 1 - 3 || alreadyTrimmedIdxes.has(idx)) { - multiplier *= .05 - } - - return base * multiplier - - } - const _findLargestByWeight = (messages: LLMChatMessage[]) => { - let largestIndex = -1 - let largestWeight = -Infinity - for (let i = 0; i < messages.length; i += 1) { - const m = messages[i] - const w = weight(m, messages, i) - if (w > largestWeight) { - largestWeight = w - largestIndex = i - } - } - return largestIndex - } - - let totalLen = 0 - for (const m of messages) { totalLen += m.content.length } - const charsNeedToTrim = totalLen - (contextWindow - maxOutputTokens) * CHARS_PER_TOKEN - if (charsNeedToTrim <= 0) return { messages } - - // <-----------------------------------------> - // 0 | | | - // | contextWindow | - // contextWindow - maxOut|putTokens - // | - // totalLen - - - // TRIM HIGHEST WEIGHT MESSAGES - let remainingCharsToTrim = charsNeedToTrim - let i = 0 - - while (remainingCharsToTrim > 0) { - i += 1 - if (i > 100) break - - const trimIdx = _findLargestByWeight(messages) - const m = messages[trimIdx] - - // if can finish here, do - const numCharsWillTrim = m.content.length - TRIM_TO_LEN - if (numCharsWillTrim > remainingCharsToTrim) { - m.content = m.content.slice(0, m.content.length - remainingCharsToTrim) - break - } - - remainingCharsToTrim -= numCharsWillTrim - m.content = m.content.substring(0, TRIM_TO_LEN - 3) + '...' - alreadyTrimmedIdxes.add(trimIdx) - } - - return { messages } - -} - - - - - - - -// no matter whether the model supports a system message or not (or what format it supports), add it in some way -const prepareMessages_addSystemInstructions = ({ - messages, - aiInstructions, - supportsSystemMessage, -}: { - messages: InternalLLMChatMessage[], - aiInstructions: string, - supportsSystemMessage: false | 'system-role' | 'developer-role' | 'separated', -}) - : { separateSystemMessageStr?: string, messages: any[] } => { - - // find system messages and concatenate them - let systemMessageStr = messages - .filter(msg => msg.role === 'system') - .map(msg => msg.content) - .join('\n') || undefined; - - if (aiInstructions) - systemMessageStr = `${(systemMessageStr ? `${systemMessageStr}\n\n` : '')}GUIDELINES\n${aiInstructions}` - - let separateSystemMessageStr: string | undefined = undefined - - // remove all system messages - const newMessages: (InternalLLMChatMessage | { role: 'developer', content: string })[] = messages.filter(msg => msg.role !== 'system') - - - // if it has a system message (if doesn't, we obviously don't care about whether it supports system message or not...) - if (systemMessageStr) { - // if supports system message - if (supportsSystemMessage) { - if (supportsSystemMessage === 'separated') - separateSystemMessageStr = systemMessageStr - else if (supportsSystemMessage === 'system-role') - newMessages.unshift({ role: 'system', content: systemMessageStr }) // add new first message - else if (supportsSystemMessage === 'developer-role') - newMessages.unshift({ role: 'developer', content: systemMessageStr }) // add new first message - } - // if does not support system message - else { - const newFirstMessage = { - role: 'user', - content: ('' - + '\n' - + systemMessageStr - + '\n' - + '\n' - + newMessages[0].content - ) - } as const - newMessages.splice(0, 1) // delete first message - newMessages.unshift(newFirstMessage) // add new first message - } - } - - return { messages: newMessages, separateSystemMessageStr } -} - - - - - -// // convert messages as if about to send to openai -// /* -// reference - https://platform.openai.com/docs/guides/function-calling#function-calling-steps -// openai MESSAGE (role=assistant): -// "tool_calls":[{ -// "type": "function", -// "id": "call_12345xyz", -// "function": { -// "name": "get_weather", -// "arguments": "{\"latitude\":48.8566,\"longitude\":2.3522}" -// }] - -// openai RESPONSE (role=user): -// { "role": "tool", -// "tool_call_id": tool_call.id, -// "content": str(result) } - -// also see -// openai on prompting - https://platform.openai.com/docs/guides/reasoning#advice-on-prompting -// openai on developer system message - https://cdn.openai.com/spec/model-spec-2024-05-08.html#follow-the-chain-of-command -// */ - -// type PrepareMessagesToolsOpenAI = ( -// Exclude | { -// role: 'assistant', -// content: string | (AnthropicReasoning | { type: 'text'; text: string })[]; -// tool_calls?: { -// type: 'function'; -// id: string; -// function: { -// name: string; -// arguments: string; -// } -// }[] -// } | { -// role: 'tool', -// tool_call_id: string; -// content: string; -// } -// )[] -// const prepareMessages_tools_openai = ({ messages }: { messages: InternalLLMChatMessage[], }) => { - -// const newMessages: PrepareMessagesToolsOpenAI = []; - -// for (let i = 0; i < messages.length; i += 1) { -// const currMsg = messages[i] - -// if (currMsg.role !== 'tool') { -// newMessages.push(currMsg) -// continue -// } - -// // edit previous assistant message to have called the tool -// const prevMsg = 0 <= i - 1 && i - 1 <= newMessages.length ? newMessages[i - 1] : undefined -// if (prevMsg?.role === 'assistant') { -// prevMsg.tool_calls = [{ -// type: 'function', -// id: currMsg.id, -// function: { -// name: currMsg.name, -// arguments: JSON.stringify(currMsg.params) -// } -// }] -// } - -// // add the tool -// newMessages.push({ -// role: 'tool', -// tool_call_id: currMsg.id, -// content: currMsg.content || EMPTY_TOOL_CONTENT, -// }) -// } -// return { messages: newMessages } - -// } - - -// // convert messages as if about to send to anthropic -// /* -// https://docs.anthropic.com/en/docs/build-with-claude/tool-use#tool-use-examples -// anthropic MESSAGE (role=assistant): -// "content": [{ -// "type": "text", -// "text": "I need to call the get_weather function, and the user wants SF, which is likely San Francisco, CA." -// }, { -// "type": "tool_use", -// "id": "toolu_01A09q90qw90lq917835lq9", -// "name": "get_weather", -// "input": { "location": "San Francisco, CA", "unit": "celsius" } -// }] -// anthropic RESPONSE (role=user): -// "content": [{ -// "type": "tool_result", -// "tool_use_id": "toolu_01A09q90qw90lq917835lq9", -// "content": "15 degrees" -// }] -// */ - -// type PrepareMessagesToolsAnthropic = ( -// Exclude | { -// role: 'assistant', -// content: string | ( -// | AnthropicReasoning -// | { -// type: 'text'; -// text: string; -// } -// | { -// type: 'tool_use'; -// name: string; -// input: Record; -// id: string; -// })[] -// } | { -// role: 'user', -// content: string | ({ -// type: 'text'; -// text: string; -// } | { -// type: 'tool_result'; -// tool_use_id: string; -// content: string; -// })[] -// } -// )[] -// /* -// Converts: - -// assistant: ...content -// tool: (id, name, params) -// -> -// assistant: ...content, call(name, id, params) -// user: ...content, result(id, content) -// */ -// const prepareMessages_tools_anthropic = ({ messages }: { messages: InternalLLMChatMessage[], }) => { -// const newMessages: PrepareMessagesToolsAnthropic = messages; - - -// for (let i = 0; i < newMessages.length; i += 1) { -// const currMsg = newMessages[i] - -// if (currMsg.role !== 'tool') continue - -// const prevMsg = 0 <= i - 1 && i - 1 <= newMessages.length ? newMessages[i - 1] : undefined - -// if (prevMsg?.role === 'assistant') { -// if (typeof prevMsg.content === 'string') prevMsg.content = [{ type: 'text', text: prevMsg.content }] -// prevMsg.content.push({ type: 'tool_use', id: currMsg.id, name: currMsg.name, input: parseObject(currMsg.params) }) -// } - -// // turn each tool into a user message with tool results at the end -// newMessages[i] = { -// role: 'user', -// content: [ -// ...[{ type: 'tool_result', tool_use_id: currMsg.id, content: currMsg.content || EMPTY_TOOL_CONTENT }] as const, -// ] -// } -// } -// return { messages: newMessages } -// } - - - - -// type PrepareMessagesTools = PrepareMessagesToolsAnthropic | PrepareMessagesToolsOpenAI - -// const prepareMessages_tools = ({ messages, supportsTools }: { messages: InternalLLMChatMessage[], supportsTools: false | 'TODO-yes-but-we-handle-it-manually' | 'anthropic-style' | 'openai-style' }): { messages: PrepareMessagesTools } => { -// if (!supportsTools) { -// return { messages: messages } -// } -// else if (supportsTools === 'anthropic-style') { -// return prepareMessages_tools_anthropic({ messages }) -// } -// else if (supportsTools === 'openai-style') { -// return prepareMessages_tools_openai({ messages }) -// } -// else { -// throw new Error(`supportsTools type not recognized`) -// } -// } - - -// remove rawAnthropicAssistantContent, and make content equal to it if supportsAnthropicContent -const prepareMessages_anthropicReasoning = ({ 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 } -} - - - - - -// do this at end -const prepareMessages_noEmptyMessage = ({ messages }: { messages: InternalLLMChatMessage[] }): { messages: InternalLLMChatMessage[] } => { - for (const currMsg of messages) { - // if content is a string, replace string with empty msg - if (typeof currMsg.content === 'string') - currMsg.content = currMsg.content || EMPTY_MESSAGE - else { - // if content is an array, replace any empty text entries with empty msg, and make sure there's at least 1 entry - for (const c of currMsg.content) { - if (c.type === 'text') c.text = c.text || EMPTY_MESSAGE - } - if (currMsg.content.length === 0) currMsg.content = [{ type: 'text', text: EMPTY_MESSAGE }] - } - } - return { messages } -} - - - -// --- CHAT --- - -export const prepareMessages = ({ - messages, - aiInstructions, - supportsSystemMessage, - supportsAnthropicReasoningSignature, - contextWindow, - maxOutputTokens, -}: { - messages: LLMChatMessage[], - aiInstructions: string, - supportsSystemMessage: false | 'system-role' | 'developer-role' | 'separated', - supportsAnthropicReasoningSignature: boolean, - contextWindow: number, - maxOutputTokens: number | null | undefined, -}) => { - maxOutputTokens = maxOutputTokens ?? 4_096 // default to 4096 - - const { messages: messages0 } = prepareMessages_normalize({ messages }) - const { messages: messages1 } = prepareMessages_fitIntoContext({ messages: messages0, contextWindow, maxOutputTokens }) - const { messages: messages2 } = prepareMessages_anthropicReasoning({ messages: messages1, supportsAnthropicReasoningSignature }) - const { messages: messages3, separateSystemMessageStr } = prepareMessages_addSystemInstructions({ messages: messages2, aiInstructions, supportsSystemMessage }) - const { messages: messages4 } = prepareMessages_noEmptyMessage({ messages: messages3 }) - - return { - messages: messages4 as any, - separateSystemMessageStr - } as const -} - - - - - - - -// --- FIM --- - -export const prepareFIMMessage = ({ - messages, - aiInstructions, -}: { - messages: LLMFIMMessage, - aiInstructions: string, -}) => { - - let prefix = `\ -${!aiInstructions ? '' : `\ -// Instructions: -// Do not output an explanation. Try to avoid outputting comments. Only output the middle code. -${aiInstructions.split('\n').map(line => `//${line}`).join('\n')}`} - -${messages.prefix}` - - const suffix = messages.suffix - const stopTokens = messages.stopTokens - const ret = { prefix, suffix, stopTokens, maxTokens: 300 } as const - return ret -} - - - - - - - -/* -Gemini has this, but they're openai-compat so we don't need to implement this -gemini request: -{ "role": "assistant", - "content": null, - "function_call": { - "name": "get_weather", - "arguments": { - "latitude": 48.8566, - "longitude": 2.3522 - } - } -} - -gemini response: -{ "role": "assistant", - "function_response": { - "name": "get_weather", - "response": { - "temperature": "15°C", - "condition": "Cloudy" - } - } -} -*/ - - - - - 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 551f0987..3db03a91 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,15 +10,13 @@ import { MistralCore } from '@mistralai/mistralai/core.js'; import { fimComplete } from '@mistralai/mistralai/funcs/fimComplete.js'; -import { LLMChatMessage, LLMFIMMessage, ModelListParams, OllamaModelResponse, OnError, OnFinalMessage, OnText } from '../../common/sendLLMMessageTypes.js'; +import { AnthropicLLMChatMessage, LLMChatMessage, LLMFIMMessage, ModelListParams, OllamaModelResponse, OnError, OnFinalMessage, OnText } from '../../common/sendLLMMessageTypes.js'; import { ChatMode, displayInfoOfProviderName, ModelSelectionOptions, ProviderName, SettingsOfProvider } from '../../common/voidSettingsTypes.js'; -import { prepareFIMMessage, prepareMessages } from './preprocessLLMMessages.js'; -import { getSendableReasoningInfo, getModelCapabilities, getProviderCapabilities, defaultProviderSettings } from '../../common/modelCapabilities.js'; +import { getSendableReasoningInfo, getModelCapabilities, getProviderCapabilities, defaultProviderSettings, getMaxOutputTokens } from '../../common/modelCapabilities.js'; import { extractReasoningWrapper, extractToolsWrapper } from './extractGrammar.js'; type InternalCommonMessageParams = { - aiInstructions: string; onText: OnText; onFinalMessage: OnFinalMessage; onError: OnError; @@ -29,8 +27,8 @@ type InternalCommonMessageParams = { _setAborter: (aborter: () => void) => void; } -type SendChatParams_Internal = InternalCommonMessageParams & { messages: LLMChatMessage[]; chatMode: ChatMode | null; } -type SendFIMParams_Internal = InternalCommonMessageParams & { messages: LLMFIMMessage; } +type SendChatParams_Internal = InternalCommonMessageParams & { messages: LLMChatMessage[]; separateSystemMessage: string | undefined; chatMode: ChatMode | null; } +type SendFIMParams_Internal = InternalCommonMessageParams & { messages: LLMFIMMessage; separateSystemMessage: string | undefined; } export type ListParams_Internal = ModelListParams @@ -96,7 +94,7 @@ const newOpenAICompatibleSDK = ({ settingsOfProvider, providerName, includeInPay } -const _sendOpenAICompatibleFIM = ({ messages: messages_, onFinalMessage, onError, settingsOfProvider, modelName: modelName_, _setAborter, providerName, aiInstructions, }: SendFIMParams_Internal) => { +const _sendOpenAICompatibleFIM = ({ messages: { prefix, suffix, stopTokens }, onFinalMessage, onError, settingsOfProvider, modelName: modelName_, _setAborter, providerName, }: SendFIMParams_Internal) => { const { modelName, supportsFIM } = getModelCapabilities(providerName, modelName_) if (!supportsFIM) { if (modelName === modelName_) @@ -106,16 +104,14 @@ const _sendOpenAICompatibleFIM = ({ messages: messages_, onFinalMessage, onError return } - const messages = prepareFIMMessage({ messages: messages_, aiInstructions, }) - const openai = newOpenAICompatibleSDK({ providerName, settingsOfProvider }) openai.completions .create({ model: modelName, - prompt: messages.prefix, - suffix: messages.suffix, - stop: messages.stopTokens, - max_tokens: messages.maxTokens, + prompt: prefix, + suffix: suffix, + stop: stopTokens, + max_tokens: 300, }) .then(async response => { const fullText = response.choices[0]?.text @@ -130,12 +126,10 @@ const _sendOpenAICompatibleFIM = ({ messages: messages_, onFinalMessage, onError -const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName: modelName_, _setAborter, providerName, aiInstructions, chatMode }: SendChatParams_Internal) => { +const _sendOpenAICompatibleChat = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName: modelName_, _setAborter, providerName, chatMode, separateSystemMessage }: SendChatParams_Internal) => { const { modelName, - supportsSystemMessage, - contextWindow, - maxOutputTokens, + specialToolFormat, reasoningCapabilities, } = getModelCapabilities(providerName, modelName_) @@ -146,15 +140,11 @@ const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage const reasoningInfo = getSendableReasoningInfo('Chat', providerName, modelName_, modelSelectionOptions) // user's modelName_ here const includeInPayload = providerReasoningIOSettings?.input?.includeInPayload?.(reasoningInfo) || {} - // max tokens - const maxTokens = reasoningInfo?.isReasoningEnabled && reasoningCapabilities ? reasoningCapabilities.reasoningMaxOutputTokens : maxOutputTokens - // instance - const { messages } = prepareMessages({ messages: messages_, aiInstructions, supportsSystemMessage, supportsAnthropicReasoningSignature: false, contextWindow, maxOutputTokens: maxTokens }) const openai: OpenAI = newOpenAICompatibleSDK({ providerName, settingsOfProvider, includeInPayload }) const options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = { model: modelName, - messages: messages, + messages: messages as any, stream: true, // max_completion_tokens: maxTokens, } @@ -168,8 +158,8 @@ const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage onFinalMessage = newOnFinalMessage } - // manually parse out tool results - if (chatMode) { + // manually parse out tool results if XML + if (!specialToolFormat) { const { newOnText, newOnFinalMessage } = extractToolsWrapper(onText, onFinalMessage, chatMode) onText = newOnText onFinalMessage = newOnFinalMessage @@ -252,13 +242,10 @@ const _openaiCompatibleList = async ({ onSuccess: onSuccess_, onError: onError_, // ------------ ANTHROPIC ------------ -const sendAnthropicChat = ({ messages: messages_, providerName, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName: modelName_, _setAborter, aiInstructions, chatMode }: SendChatParams_Internal) => { +const sendAnthropicChat = ({ messages, providerName, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName: modelName_, _setAborter, separateSystemMessage, chatMode }: SendChatParams_Internal) => { const { modelName, - supportsSystemMessage, - contextWindow, - maxOutputTokens, - reasoningCapabilities, + specialToolFormat, } = getModelCapabilities(providerName, modelName_) const thisConfig = settingsOfProvider.anthropic @@ -269,25 +256,24 @@ const sendAnthropicChat = ({ messages: messages_, providerName, onText, onFinalM const includeInPayload = providerReasoningIOSettings?.input?.includeInPayload?.(reasoningInfo) || {} // anthropic-specific - max tokens - const maxTokens = reasoningInfo?.isReasoningEnabled && reasoningCapabilities ? reasoningCapabilities.reasoningMaxOutputTokens : maxOutputTokens + const maxTokens = getMaxOutputTokens(providerName, modelName_, { isReasoningEnabled: !!reasoningInfo?.isReasoningEnabled }) // instance - const { messages, separateSystemMessageStr } = prepareMessages({ messages: messages_, aiInstructions, supportsSystemMessage, supportsAnthropicReasoningSignature: true, contextWindow, maxOutputTokens: maxTokens }) const anthropic = new Anthropic({ apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true }); const stream = anthropic.messages.stream({ - system: separateSystemMessageStr, - messages: messages, + system: separateSystemMessage ?? undefined, + messages: messages as AnthropicLLMChatMessage[], model: modelName, max_tokens: maxTokens ?? 4_096, // anthropic requires this ...includeInPayload, }) // manually parse out tool results - if (chatMode) { + if (!specialToolFormat) { const { newOnText, newOnFinalMessage } = extractToolsWrapper(onText, onFinalMessage, chatMode) onText = newOnText onFinalMessage = newOnFinalMessage @@ -360,7 +346,7 @@ const sendAnthropicChat = ({ messages: messages_, providerName, onText, onFinalM // ------------ MISTRAL ------------ // https://docs.mistral.ai/api/#tag/fim -const sendMistralFIM = ({ messages: messages_, onFinalMessage, onError, settingsOfProvider, modelName: modelName_, _setAborter, providerName, aiInstructions, modelSelectionOptions }: SendFIMParams_Internal) => { +const sendMistralFIM = ({ messages, onFinalMessage, onError, settingsOfProvider, modelName: modelName_, _setAborter, providerName }: SendFIMParams_Internal) => { const { modelName, supportsFIM } = getModelCapabilities(providerName, modelName_) if (!supportsFIM) { if (modelName === modelName_) @@ -369,7 +355,6 @@ const sendMistralFIM = ({ messages: messages_, onFinalMessage, onError, settings onError({ message: `Model ${modelName_} (${modelName}) does not support FIM.`, fullError: null }) return } - const messages = prepareFIMMessage({ messages: messages_, aiInstructions }) const mistral = new MistralCore({ apiKey: settingsOfProvider.mistral.apiKey }) fimComplete(mistral, @@ -378,7 +363,7 @@ const sendMistralFIM = ({ messages: messages_, onFinalMessage, onError, settings prompt: messages.prefix, suffix: messages.suffix, stream: false, - maxTokens: messages.maxTokens, + maxTokens: 300, stop: messages.stopTokens, }) .then(async response => { @@ -426,12 +411,10 @@ const ollamaList = async ({ onSuccess: onSuccess_, onError: onError_, settingsOf } } -const sendOllamaFIM = ({ messages: messages_, onFinalMessage, onError, settingsOfProvider, modelName, aiInstructions, _setAborter }: SendFIMParams_Internal) => { +const sendOllamaFIM = ({ messages, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }: SendFIMParams_Internal) => { const thisConfig = settingsOfProvider.ollama const ollama = newOllamaSDK({ endpoint: thisConfig.endpoint }) - const messages = prepareFIMMessage({ messages: messages_, aiInstructions, }) - let fullText = '' ollama.generate({ model: modelName, @@ -439,7 +422,7 @@ const sendOllamaFIM = ({ messages: messages_, onFinalMessage, onError, settingsO suffix: messages.suffix, options: { stop: messages.stopTokens, - num_predict: messages.maxTokens, // max tokens + num_predict: 300, // max tokens // repeat_penalty: 1, }, raw: true, diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts index 88bb1ad7..1554aeeb 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts @@ -11,7 +11,6 @@ import { sendLLMMessageToProviderImplementation } from './sendLLMMessage.impl.js export const sendLLMMessage = ({ messagesType, - aiInstructions, messages: messages_, onText: onText_, onFinalMessage: onFinalMessage_, @@ -22,6 +21,7 @@ export const sendLLMMessage = ({ modelSelection, modelSelectionOptions, chatMode, + separateSystemMessage, }: SendLLMMessageParams, metricsService: IMetricsService @@ -108,12 +108,12 @@ export const sendLLMMessage = ({ } const { sendFIM, sendChat } = implementation if (messagesType === 'chatMessages') { - sendChat({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName, _setAborter, providerName, aiInstructions, chatMode }) + sendChat({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName, _setAborter, providerName, separateSystemMessage, chatMode }) return } if (messagesType === 'FIMMessage') { if (sendFIM) { - sendFIM({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName, _setAborter, providerName, aiInstructions }) + sendFIM({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName, _setAborter, providerName, separateSystemMessage }) return } onError({ message: `Error: This provider does not support Autocomplete yet.`, fullError: null }) From a1896293418513ea0a698f51b39d5137143c8983 Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Mon, 14 Apr 2025 22:46:08 -0700 Subject: [PATCH 05/12] fix current file --- .../contrib/void/browser/chatThreadService.ts | 42 +++++++++++++--- .../contrib/void/browser/sidebarActions.ts | 49 ++++--------------- 2 files changed, 45 insertions(+), 46 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 34b499ca..cb2816ee 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -35,6 +35,32 @@ import { truncate } from '../../../../base/common/strings.js'; import { THREAD_STORAGE_KEY } from '../common/storageKeys.js'; import { IConvertToLLMMessageService } from './convertToLLMMessageService.js'; +export const findStagingSelectionIndex = (currentSelections: StagingSelectionItem[] | undefined, newSelection: StagingSelectionItem): number | null => { + if (!currentSelections) return null + + for (let i = 0; i < currentSelections.length; i += 1) { + const s = currentSelections[i] + + if (s.uri.fsPath !== newSelection.uri.fsPath) continue + + if (s.type === 'File' && newSelection.type === 'File') { + return i + } + if (s.type === 'CodeSelection' && newSelection.type === 'CodeSelection') { + if (s.uri.fsPath !== newSelection.uri.fsPath) continue + // if there's any collision return true + const [oldStart, oldEnd] = s.range + const [newStart, newEnd] = newSelection.range + if (oldStart !== newStart || oldEnd !== newEnd) continue + return i + } + if (s.type === 'Folder' && newSelection.type === 'Folder') { + return i + } + } + return null +} + /* @@ -281,14 +307,16 @@ class ChatThreadService extends Disposable implements IChatThreadService { } const oldStagingSelections = this.getCurrentThreadState().stagingSelections || []; - const fileIsAlreadyHere = oldStagingSelections.some(s => s.type === 'File' && s.uri.fsPath === newStagingSelection.uri.fsPath) - if (fileIsAlreadyHere) return - // remove all old selectons that are marked as `wasAddedAsCurrentFile`, and add new selection - const newStagingSelections: StagingSelectionItem[] = [ - ...oldStagingSelections.filter(s => !s.state?.wasAddedAsCurrentFile), - newStagingSelection - ] + // remove all old selectons that are marked as `wasAddedAsCurrentFile` + const newStagingSelections: StagingSelectionItem[] = oldStagingSelections.filter(s => s.state && !s.state.wasAddedAsCurrentFile) + + const fileIsAlreadyHere = oldStagingSelections.some(s => s.type === 'File' && s.uri.fsPath === newStagingSelection.uri.fsPath) + + if (!fileIsAlreadyHere) { + newStagingSelections.push(newStagingSelection) + } + this.setCurrentThreadState({ stagingSelections: newStagingSelections }); } diff --git a/src/vs/workbench/contrib/void/browser/sidebarActions.ts b/src/vs/workbench/contrib/void/browser/sidebarActions.ts index 7bcb0ff0..0c2db06b 100644 --- a/src/vs/workbench/contrib/void/browser/sidebarActions.ts +++ b/src/vs/workbench/contrib/void/browser/sidebarActions.ts @@ -24,7 +24,7 @@ import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; import { localize2 } from '../../../../nls.js'; import { StagingSelectionItem } from '../common/chatThreadServiceTypes.js'; -import { IChatThreadService } from './chatThreadService.js'; +import { findStagingSelectionIndex, IChatThreadService } from './chatThreadService.js'; // ---------- Register commands and keybindings ---------- @@ -63,31 +63,6 @@ export const roundRangeToLines = (range: IRange | null | undefined, options: { e // } -const findStagingItemToReplace = (currentSelections: StagingSelectionItem[] | undefined, newSelection: StagingSelectionItem): [number, StagingSelectionItem] | null => { - if (!currentSelections) return null - - for (let i = 0; i < currentSelections.length; i += 1) { - const s = currentSelections[i] - - if (s.uri.fsPath !== newSelection.uri.fsPath) continue - - if (s.type === 'File' && newSelection.type === 'File') { - return [i, s] as const - } - if (s.type === 'CodeSelection' && newSelection.type === 'CodeSelection') { - if (s.uri.fsPath !== newSelection.uri.fsPath) continue - // if there's any collision return true - const [oldStart, oldEnd] = s.range - const [newStart, newEnd] = newSelection.range - if (oldStart !== newStart || oldEnd !== newEnd) continue - return [i, s] as const - } - if (s.type === 'Folder' && newSelection.type === 'Folder') { - return [i, s] as const - } - } - return null -} const VOID_OPEN_SIDEBAR_ACTION_ID = 'void.sidebar.open' registerAction2(class extends Action2 { @@ -132,7 +107,7 @@ registerAction2(class extends Action2 { } - const selection: StagingSelectionItem = !selectionRange || (selectionRange.startLineNumber > selectionRange.endLineNumber) ? { + const newSelection: StagingSelectionItem = !selectionRange || (selectionRange.startLineNumber > selectionRange.endLineNumber) ? { type: 'File', uri: model.uri, language: model.getLanguageId(), @@ -163,21 +138,17 @@ registerAction2(class extends Action2 { } // if matches with existing selection, overwrite (since text may change) - const replaceRes = findStagingItemToReplace(selections, selection) - if (replaceRes) { - const [idx, newSel] = replaceRes - - if (idx !== undefined && idx !== -1) { - setSelections([ - ...selections!.slice(0, idx), - newSel, - ...selections!.slice(idx + 1, Infinity) - ]) - } + const idx = findStagingSelectionIndex(selections, newSelection) + if (idx !== null && idx !== -1) { + setSelections([ + ...selections!.slice(0, idx), + newSelection, + ...selections!.slice(idx + 1, Infinity) + ]) } // if no match, add it else { - setSelections([...(selections ?? []), selection]) + setSelections([...(selections ?? []), newSelection]) } } From 922f3ba2d72ef5d3541e0d6619003e3d5e4ab436 Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Mon, 14 Apr 2025 22:46:52 -0700 Subject: [PATCH 06/12] fix current file --- .../contrib/void/browser/sidebarActions.ts | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/vs/workbench/contrib/void/browser/sidebarActions.ts b/src/vs/workbench/contrib/void/browser/sidebarActions.ts index 0c2db06b..fe9a9d61 100644 --- a/src/vs/workbench/contrib/void/browser/sidebarActions.ts +++ b/src/vs/workbench/contrib/void/browser/sidebarActions.ts @@ -29,6 +29,32 @@ import { findStagingSelectionIndex, IChatThreadService } from './chatThreadServi // ---------- Register commands and keybindings ---------- +export const findStagingSelectionIndex = (currentSelections: StagingSelectionItem[] | undefined, newSelection: StagingSelectionItem): number | null => { + if (!currentSelections) return null + + for (let i = 0; i < currentSelections.length; i += 1) { + const s = currentSelections[i] + + if (s.uri.fsPath !== newSelection.uri.fsPath) continue + + if (s.type === 'File' && newSelection.type === 'File') { + return i + } + if (s.type === 'CodeSelection' && newSelection.type === 'CodeSelection') { + if (s.uri.fsPath !== newSelection.uri.fsPath) continue + // if there's any collision return true + const [oldStart, oldEnd] = s.range + const [newStart, newEnd] = newSelection.range + if (oldStart !== newStart || oldEnd !== newEnd) continue + return i + } + if (s.type === 'Folder' && newSelection.type === 'Folder') { + return i + } + } + return null +} + export const roundRangeToLines = (range: IRange | null | undefined, options: { emptySelectionBehavior: 'null' | 'line' }) => { if (!range) return null From 5ac03bb9178b7a539ffb6daa34ce60a1865990e6 Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Mon, 14 Apr 2025 22:47:10 -0700 Subject: [PATCH 07/12] fix --- src/vs/workbench/contrib/void/browser/sidebarActions.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/sidebarActions.ts b/src/vs/workbench/contrib/void/browser/sidebarActions.ts index fe9a9d61..e1476552 100644 --- a/src/vs/workbench/contrib/void/browser/sidebarActions.ts +++ b/src/vs/workbench/contrib/void/browser/sidebarActions.ts @@ -24,12 +24,12 @@ import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; import { localize2 } from '../../../../nls.js'; import { StagingSelectionItem } from '../common/chatThreadServiceTypes.js'; -import { findStagingSelectionIndex, IChatThreadService } from './chatThreadService.js'; +import { IChatThreadService } from './chatThreadService.js'; // ---------- Register commands and keybindings ---------- -export const findStagingSelectionIndex = (currentSelections: StagingSelectionItem[] | undefined, newSelection: StagingSelectionItem): number | null => { +const findStagingSelectionIndex = (currentSelections: StagingSelectionItem[] | undefined, newSelection: StagingSelectionItem): number | null => { if (!currentSelections) return null for (let i = 0; i < currentSelections.length; i += 1) { From 499a47904ec4f68ffc36c91410a1cf3d6ea658bd Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Mon, 14 Apr 2025 22:53:39 -0700 Subject: [PATCH 08/12] style --- .../contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 5f4193f0..88fff3b3 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 @@ -1298,7 +1298,7 @@ const ToolRequestAcceptRejectButtons = () => { ) - return
+ return
{approveButton} {cancelButton} {autoApproveToggle} From ee959ded08194b9ae2837a1f05f9d8b423582cf8 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Mon, 14 Apr 2025 22:57:27 -0700 Subject: [PATCH 09/12] 4.1 and 3.7 work! --- .../react/src/sidebar-tsx/SidebarChat.tsx | 4 +- .../contrib/void/common/prompt/prompts.ts | 33 ---- .../llmMessage/extractGrammar.ts | 4 +- .../llmMessage/sendLLMMessage.impl.ts | 151 ++++++++++++++++-- 4 files changed, 138 insertions(+), 54 deletions(-) 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) => { From ba8644fbb6d4427c93b9b99512d4e444881d5937 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Mon, 14 Apr 2025 23:12:10 -0700 Subject: [PATCH 10/12] read_file improvements --- .../void/browser/react/src/sidebar-tsx/SidebarChat.tsx | 7 ++++--- src/vs/workbench/contrib/void/browser/toolsService.ts | 6 +++--- .../workbench/contrib/void/common/toolsServiceTypes.ts | 2 +- .../electron-main/llmMessage/sendLLMMessage.impl.ts | 10 ++++++---- 4 files changed, 14 insertions(+), 11 deletions(-) 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 2f1c7ae6..4892feff 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 @@ -29,6 +29,7 @@ import { acceptAllBg, acceptBorder, buttonFontSize, buttonTextColor, rejectAllBg import { ToolName, toolNames } from '../../../../common/prompt/prompts.js'; import { error } from 'console'; import { RawToolCallObj } from '../../../../common/sendLLMMessageTypes.js'; +import { MAX_FILE_CHARS_PAGE } from '../../../toolsService.js'; @@ -1434,8 +1435,8 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, const componentParams: ToolHeaderParams = { title, desc1, isError, icon } if (toolMessage.params.startLine !== null || toolMessage.params.endLine !== null) { - const start = toolMessage.params.startLine === null ? `start` : `${toolMessage.params.startLine}` - const end = toolMessage.params.endLine === null ? `end` : `${toolMessage.params.endLine}` + const start = toolMessage.params.startLine === null ? `1` : `${toolMessage.params.startLine}` + const end = toolMessage.params.endLine === null ? `` : `${toolMessage.params.endLine}` const addStr = `(${start}-${end})` componentParams.desc1 += ` ${addStr}` } @@ -1444,7 +1445,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, const { params, result } = toolMessage componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) } if (result.hasNextPage && params.pageNumber === 1) // first page - componentParams.desc2 = '(truncated)' + componentParams.desc2 = `(first ${Math.round(MAX_FILE_CHARS_PAGE) / 1000}k)` else if (params.pageNumber > 1) // subsequent pages componentParams.desc2 = `(part ${params.pageNumber})` } diff --git a/src/vs/workbench/contrib/void/browser/toolsService.ts b/src/vs/workbench/contrib/void/browser/toolsService.ts index 53a33daf..cc37f836 100644 --- a/src/vs/workbench/contrib/void/browser/toolsService.ts +++ b/src/vs/workbench/contrib/void/browser/toolsService.ts @@ -276,8 +276,8 @@ export class ToolsService implements IToolsService { const toIdx = MAX_FILE_CHARS_PAGE * pageNumber - 1 const fileContents = contents.slice(fromIdx, toIdx + 1) // paginate const hasNextPage = (contents.length - 1) - toIdx >= 1 - - return { result: { fileContents, hasNextPage } } + const totalFileLen = contents.length + return { result: { fileContents, totalFileLen, hasNextPage } } }, ls_dir: async ({ rootURI, pageNumber }) => { @@ -400,7 +400,7 @@ export class ToolsService implements IToolsService { // given to the LLM after the call this.stringOfResult = { read_file: (params, result) => { - return result.fileContents + nextPageStr(result.hasNextPage) + return `${result.fileContents}${nextPageStr(result.hasNextPage)}${result.hasNextPage ? `This file has ${result.totalFileLen} characters, paginated ${MAX_FILE_CHARS_PAGE} at a time.` : ''}` }, ls_dir: (params, result) => { const dirTreeStr = stringifyDirectoryTree1Deep(params, result) diff --git a/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts b/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts index 891db752..a8589e0f 100644 --- a/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts +++ b/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts @@ -39,7 +39,7 @@ export type ToolCallParams = { // RESULT OF TOOL CALL export type ToolResultType = { - 'read_file': { fileContents: string, hasNextPage: boolean }, + 'read_file': { fileContents: string, totalFileLen: number, hasNextPage: boolean }, 'ls_dir': { children: ShallowDirectoryItem[] | null, hasNextPage: boolean, hasPrevPage: boolean, itemsRemaining: number }, 'get_dir_structure': { str: string, }, 'search_pathnames_only': { uris: URI[], hasNextPage: boolean }, 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 5f7f0474..687497d0 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 @@ -263,8 +263,9 @@ const _sendOpenAICompatibleChat = ({ messages, onText, onFinalMessage, onError, onError({ message: 'Void: Response from model was empty.', fullError: null }) } else { - const toolCall = openAIToolToRawToolCallObj(toolName, toolParamsStr, toolId) ?? undefined - onFinalMessage({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar, anthropicReasoning: null, toolCall: toolCall }); + const toolCall = openAIToolToRawToolCallObj(toolName, toolParamsStr, toolId) + const toolCallObj = toolCall ? { toolCall } : {} + onFinalMessage({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar, anthropicReasoning: null, ...toolCallObj }); } }) // when error/fail - this catches errors of both .create() and .then(for await) @@ -448,8 +449,9 @@ const sendAnthropicChat = ({ messages, providerName, onText, onFinalMessage, onE stream.on('finalMessage', (response) => { const anthropicReasoning = response.content.filter(c => c.type === 'thinking' || c.type === 'redacted_thinking') const tools = response.content.filter(c => c.type === 'tool_use') - const toolCall = tools[0] ? anthropicToolToRawToolCallObj(tools[0]) ?? undefined : undefined - onFinalMessage({ fullText, fullReasoning, anthropicReasoning, toolCall, }) + const toolCall = tools[0] && anthropicToolToRawToolCallObj(tools[0]) + const toolCallObj = toolCall ? { toolCall } : {} + onFinalMessage({ fullText, fullReasoning, anthropicReasoning, ...toolCallObj }) }) // on error stream.on('error', (error) => { From 18b76885f54ad3ba5bcc8f2e53cec8843dbc1abf Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Mon, 14 Apr 2025 23:17:33 -0700 Subject: [PATCH 11/12] 1.2.1 --- product.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/product.json b/product.json index ab0485bd..aee6d2c9 100644 --- a/product.json +++ b/product.json @@ -1,7 +1,7 @@ { "nameShort": "Void", "nameLong": "Void", - "voidVersion": "1.2.0", + "voidVersion": "1.2.1", "applicationName": "void", "dataFolderName": ".void-editor", "win32MutexName": "voideditor", From 6ce37f7627baaf37fde99dd48c09b1a0cc677799 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Mon, 14 Apr 2025 23:47:14 -0700 Subject: [PATCH 12/12] misc --- .../react/src/markdown/ChatMarkdownRender.tsx | 60 +++++++++---------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx index acf731f4..91773cf6 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx @@ -57,7 +57,7 @@ const CodespanWithLink = ({ text, rawText, chatMessageLocation }: { text: string const [didComputeCodespanLink, setDidComputeCodespanLink] = useState(false) let link = undefined - if (rawText.endsWith("`")) { // if codespan was completed + if (rawText.endsWith('`')) { // if codespan was completed // get link from cache link = chatThreadService.getCodespanLink({ codespanStr: text, messageIdx, threadId }) @@ -120,11 +120,11 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, .. return null; } - if (t.type === "space") { + if (t.type === 'space') { return {t.raw} } - if (t.type === "code") { + if (t.type === 'code') { const [firstLine, remainingContents] = separateOutFirstLine(t.text) const firstLineIsURI = isValidUri(firstLine) && !codeURI const contents = firstLineIsURI ? (remainingContents?.trimStart() || '') : t.text // exclude first-line URI from contents @@ -152,7 +152,7 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, .. } if (options.isApplyEnabled && chatMessageLocation) { - const isCodeblockClosed = t.raw.trimEnd().endsWith('```') // user should only be able to Apply when the code has been closed (t.raw ends with "```") + const isCodeblockClosed = t.raw.trimEnd().endsWith('```') // user should only be able to Apply when the code has been closed (t.raw ends with '```') const applyBoxId = getApplyBoxId({ threadId: chatMessageLocation.threadId, @@ -179,7 +179,7 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, .. /> } - if (t.type === "heading") { + if (t.type === 'heading') { const HeadingTag = `h${t.depth}` as keyof JSX.IntrinsicElements @@ -188,7 +188,7 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, .. } - if (t.type === "table") { + if (t.type === 'table') { return (
@@ -217,14 +217,14 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, .. ) // return ( //
- //
+ //
// - // + // // {t.header.map((cell: any, index: number) => ( // @@ -237,8 +237,8 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, .. // {row.map((cell: any, cellIndex: number) => ( // @@ -251,32 +251,32 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, .. // ) } - if (t.type === "hr") { + if (t.type === 'hr') { return
} - if (t.type === "blockquote") { + if (t.type === 'blockquote') { return
{t.text}
} if (t.type === 'list_item') { return
  • - +
  • } - if (t.type === "list") { - const ListTag = t.ordered ? "ol" : "ul" + if (t.type === 'list') { + const ListTag = t.ordered ? 'ol' : 'ul' return ( {t.items.map((item, index) => (
  • {item.task && ( - + )} @@ -287,7 +287,7 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, .. ) } - if (t.type === "paragraph") { + if (t.type === 'paragraph') { const contents = <> {t.tokens.map((token, index) => ( {contents}

    } - if (t.type === "text" || t.type === "escape") { + if (t.type === 'text' || t.type === 'escape' || t.type === 'html') { return {t.raw} } - if (t.type === "def") { + if (t.type === 'def') { return <> // Definitions are typically not rendered } - if (t.type === "link") { + if (t.type === 'link') { return ( { window.open(t.href) }} @@ -325,7 +325,7 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, .. ) } - if (t.type === "image") { + if (t.type === 'image') { return {t.text} } - if (t.type === "strong") { + if (t.type === 'strong') { return {t.text} } - if (t.type === "em") { + if (t.type === 'em') { return {t.text} } // inline code - if (t.type === "codespan" || t.type === "html") { + if (t.type === 'codespan') { if (options.isLinkDetectionEnabled && chatMessageLocation) { return } - if (t.type === "br") { + if (t.type === 'br') { return
    } // strikethrough - if (t.type === "del") { + if (t.type === 'del') { return {t.text} } // default return ( -
    - Unknown token rendered... +
    + Unknown token rendered...
    ) }
  • // {cell.raw} // // {cell.raw} //