Merge pull request #400 from voideditor/model-selection

GPT 4.1 and Claude 3.7 Thinking with Tools
This commit is contained in:
Andrew Pareles 2025-04-14 23:52:58 -07:00 committed by GitHub
commit eda8517c78
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 1072 additions and 921 deletions

View file

@ -1,7 +1,7 @@
{ {
"nameShort": "Void", "nameShort": "Void",
"nameLong": "Void", "nameLong": "Void",
"voidVersion": "1.2.0", "voidVersion": "1.2.1",
"applicationName": "void", "applicationName": "void",
"dataFolderName": ".void-editor", "dataFolderName": ".void-editor",
"win32MutexName": "voideditor", "win32MutexName": "voideditor",

View file

@ -20,6 +20,7 @@ import { ILLMMessageService } from '../common/sendLLMMessageService.js';
import { isWindows } from '../../../../base/common/platform.js'; import { isWindows } from '../../../../base/common/platform.js';
import { IVoidSettingsService } from '../common/voidSettingsService.js'; import { IVoidSettingsService } from '../common/voidSettingsService.js';
import { FeatureName } from '../common/voidSettingsTypes.js'; import { FeatureName } from '../common/voidSettingsTypes.js';
import { IConvertToLLMMessageService } from './convertToLLMMessageService.js';
// import { IContextGatheringService } from './contextGatheringService.js'; // import { IContextGatheringService } from './contextGatheringService.js';
@ -791,18 +792,21 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
const featureName: FeatureName = 'Autocomplete' const featureName: FeatureName = 'Autocomplete'
const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName] const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName]
const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[featureName][modelSelection.providerName]?.[modelSelection.modelName] : undefined 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 // set parameters of `newAutocompletion` appropriately
newAutocompletion.llmPromise = new Promise((resolve, reject) => { newAutocompletion.llmPromise = new Promise((resolve, reject) => {
const requestId = this._llmMessageService.sendLLMMessage({ const requestId = this._llmMessageService.sendLLMMessage({
messagesType: 'FIMMessage', messagesType: 'FIMMessage',
messages: { messages: this._convertToLLMMessageService.prepareFIMMessage({
prefix: llmPrefix, messages: {
suffix: llmSuffix, prefix: llmPrefix,
stopTokens: stopTokens, suffix: llmSuffix,
}, stopTokens: stopTokens,
},
aiInstructions
}),
modelSelection, modelSelection,
modelSelectionOptions, modelSelectionOptions,
logging: { loggingName: 'Autocomplete' }, logging: { loggingName: 'Autocomplete' },
@ -890,6 +894,7 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
@IEditorService private readonly _editorService: IEditorService, @IEditorService private readonly _editorService: IEditorService,
@IModelService private readonly _modelService: IModelService, @IModelService private readonly _modelService: IModelService,
@IVoidSettingsService private readonly _settingsService: IVoidSettingsService, @IVoidSettingsService private readonly _settingsService: IVoidSettingsService,
@IConvertToLLMMessageService private readonly _convertToLLMMessageService: IConvertToLLMMessageService
// @IContextGatheringService private readonly _contextGatheringService: IContextGatheringService, // @IContextGatheringService private readonly _contextGatheringService: IContextGatheringService,
) { ) {
super() super()

View file

@ -11,11 +11,10 @@ import { IStorageService, StorageScope, StorageTarget } from '../../../../platfo
import { URI } from '../../../../base/common/uri.js'; import { URI } from '../../../../base/common/uri.js';
import { Emitter, Event } from '../../../../base/common/event.js'; import { Emitter, Event } from '../../../../base/common/event.js';
import { ILLMMessageService } from '../common/sendLLMMessageService.js'; import { ILLMMessageService } from '../common/sendLLMMessageService.js';
import { chat_userMessageContent, chat_systemMessage, ToolName, toolCallXMLStr, } from '../common/prompt/prompts.js'; import { chat_userMessageContent, ToolName, } from '../common/prompt/prompts.js';
import { getErrorMessage, LLMChatMessage, RawToolCallObj, RawToolParamsObj } from '../common/sendLLMMessageTypes.js'; import { getErrorMessage, RawToolCallObj, RawToolParamsObj } from '../common/sendLLMMessageTypes.js';
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
import { generateUuid } from '../../../../base/common/uuid.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 { IVoidSettingsService } from '../common/voidSettingsService.js';
import { ToolCallParams, ToolResultType, toolNamesThatRequireApproval } from '../common/toolsServiceTypes.js'; import { ToolCallParams, ToolResultType, toolNamesThatRequireApproval } from '../common/toolsServiceTypes.js';
import { IToolsService } from './toolsService.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 { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js';
import { ChatMessage, CheckpointEntry, CodespanLocationLink, StagingSelectionItem, ToolMessage } from '../common/chatThreadServiceTypes.js'; import { ChatMessage, CheckpointEntry, CodespanLocationLink, StagingSelectionItem, ToolMessage } from '../common/chatThreadServiceTypes.js';
import { Position } from '../../../../editor/common/core/position.js'; import { Position } from '../../../../editor/common/core/position.js';
import { ITerminalToolService } from './terminalToolService.js';
import { IMetricsService } from '../common/metricsService.js'; import { IMetricsService } from '../common/metricsService.js';
import { shorten } from '../../../../base/common/labels.js'; import { shorten } from '../../../../base/common/labels.js';
import { IVoidModelService } from '../common/voidModelService.js'; import { IVoidModelService } from '../common/voidModelService.js';
@ -33,11 +31,35 @@ import { findLast, findLastIdx } from '../../../../base/common/arraysFind.js';
import { IEditCodeService } from './editCodeServiceInterface.js'; import { IEditCodeService } from './editCodeServiceInterface.js';
import { VoidFileSnapshot } from '../common/editCodeServiceTypes.js'; import { VoidFileSnapshot } from '../common/editCodeServiceTypes.js';
import { INotificationService, Severity } from '../../../../platform/notification/common/notification.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 { truncate } from '../../../../base/common/strings.js';
import { THREAD_STORAGE_KEY } from '../common/storageKeys.js'; import { THREAD_STORAGE_KEY } from '../common/storageKeys.js';
import { deepClone } from '../../../../base/common/objects.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
}
/* /*
@ -221,17 +243,14 @@ class ChatThreadService extends Disposable implements IChatThreadService {
@IVoidModelService private readonly _voidModelService: IVoidModelService, @IVoidModelService private readonly _voidModelService: IVoidModelService,
@ILLMMessageService private readonly _llmMessageService: ILLMMessageService, @ILLMMessageService private readonly _llmMessageService: ILLMMessageService,
@IToolsService private readonly _toolsService: IToolsService, @IToolsService private readonly _toolsService: IToolsService,
@IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService,
@IVoidSettingsService private readonly _settingsService: IVoidSettingsService, @IVoidSettingsService private readonly _settingsService: IVoidSettingsService,
@ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService, @ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService,
@ITerminalToolService private readonly _terminalToolService: ITerminalToolService,
@IMetricsService private readonly _metricsService: IMetricsService, @IMetricsService private readonly _metricsService: IMetricsService,
@IEditorService private readonly _editorService: IEditorService, @IEditorService private readonly _editorService: IEditorService,
@ICodeEditorService private readonly _codeEditorService: ICodeEditorService, @ICodeEditorService private readonly _codeEditorService: ICodeEditorService,
@IEditCodeService private readonly _editCodeService: IEditCodeService, @IEditCodeService private readonly _editCodeService: IEditCodeService,
@INotificationService private readonly _notificationService: INotificationService, @INotificationService private readonly _notificationService: INotificationService,
@IModelService private readonly _modelService: IModelService, @IConvertToLLMMessageService private readonly _convertToLLMMessagesService: IConvertToLLMMessageService,
@IDirectoryStrService private readonly _directoryStrService: IDirectoryStrService,
) { ) {
super() super()
this.state = { allThreads: {}, currentThreadId: null as unknown as string } // default state this.state = { allThreads: {}, currentThreadId: null as unknown as string } // default state
@ -288,14 +307,16 @@ class ChatThreadService extends Disposable implements IChatThreadService {
} }
const oldStagingSelections = this.getCurrentThreadState().stagingSelections || []; 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 // remove all old selectons that are marked as `wasAddedAsCurrentFile`
const newStagingSelections: StagingSelectionItem[] = [ const newStagingSelections: StagingSelectionItem[] = oldStagingSelections.filter(s => s.state && !s.state.wasAddedAsCurrentFile)
...oldStagingSelections.filter(s => !s.state?.wasAddedAsCurrentFile),
newStagingSelection const fileIsAlreadyHere = oldStagingSelections.some(s => s.type === 'File' && s.uri.fsPath === newStagingSelection.uri.fsPath)
]
if (!fileIsAlreadyHere) {
newStagingSelections.push(newStagingSelection)
}
this.setCurrentThreadState({ stagingSelections: newStagingSelections }); this.setCurrentThreadState({ stagingSelections: newStagingSelections });
} }
@ -454,10 +475,10 @@ class ChatThreadService extends Disposable implements IChatThreadService {
} }
else return else return
const { name } = lastMsg const { name, id, rawParams } = lastMsg
const errorMessage = this.errMsgs.rejected 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') this._setStreamState(threadId, {}, 'set')
} }
@ -482,7 +503,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
const llmCancelToken = this.streamState[threadId]?.streamingToken const llmCancelToken = this.streamState[threadId]?.streamingToken
if (llmCancelToken !== undefined) { this._llmMessageService.abort(llmCancelToken) } 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) { if (toolCallSoFar) {
this._addMessageToThread(threadId, { role: 'interrupted_streaming_tool', name: toolCallSoFar.name }) this._addMessageToThread(threadId, { role: 'interrupted_streaming_tool', name: toolCallSoFar.name })
@ -505,69 +526,12 @@ class ChatThreadService extends Disposable implements IChatThreadService {
private readonly _currentlyRunningToolInterruptor: { [threadId: string]: (() => void) | undefined } = {} 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</${c.name}_result>`
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 // returns true when the tool call is waiting for user approval
private _runToolCall = async ( private _runToolCall = async (
threadId: string, threadId: string,
toolName: ToolName, 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 }> => { ): Promise<{ awaitingUserApproval?: boolean, interrupted?: boolean }> => {
// compute these below // compute these below
@ -582,7 +546,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
toolParams = params toolParams = params
} catch (error) { } catch (error) {
const errorMessage = getErrorMessage(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 {} return {}
} }
// once validated, add checkpoint for edit // once validated, add checkpoint for edit
@ -593,7 +557,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
if (toolRequiresApproval) { if (toolRequiresApproval) {
const autoApprove = this._settingsService.state.globalSettings.autoApprove 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) // 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) { if (!autoApprove) {
return { awaitingUserApproval: true } return { awaitingUserApproval: true }
} }
@ -603,9 +567,11 @@ class ChatThreadService extends Disposable implements IChatThreadService {
toolParams = opts.validatedParams toolParams = opts.validatedParams
} }
// 3. call the tool // 3. call the tool
this._setStreamState(threadId, { isRunning: 'tool' }, 'merge') 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 let interrupted = false
try { try {
@ -624,7 +590,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
const errorMessage = getErrorMessage(error) 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 {} return {}
} }
@ -633,12 +599,12 @@ class ChatThreadService extends Disposable implements IChatThreadService {
toolResultStr = this._toolsService.stringOfResult[toolName](toolParams as any, toolResult as any) toolResultStr = this._toolsService.stringOfResult[toolName](toolParams as any, toolResult as any)
} catch (error) { } catch (error) {
const errorMessage = this.errMsgs.errWhenStringifying(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 {} return {}
} }
// 5. add to history and keep going // 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 {} return {}
}; };
@ -659,6 +625,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
callThisToolFirst?: ToolMessage<ToolName> & { type: 'tool_request' } callThisToolFirst?: ToolMessage<ToolName> & { type: 'tool_request' }
}) { }) {
// above just defines helpers, below starts the actual function // 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 const { chatMode } = this._settingsService.state.globalSettings // should not change as we loop even if user changes it, so it goes here
@ -672,7 +639,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
// before enter loop, call tool // before enter loop, call tool
if (callThisToolFirst) { 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 if (interrupted) return
} }
@ -688,34 +655,36 @@ class ChatThreadService extends Disposable implements IChatThreadService {
// send llm message // send llm message
this._setStreamState(threadId, { isRunning: 'LLM' }, 'merge') this._setStreamState(threadId, { isRunning: 'LLM' }, 'merge')
const systemMessage = await this._generateSystemMessage(chatMode)
const llmMessages = await this._generateLLMMessages(threadId) const chatMessages = this.state.allThreads[threadId]?.messages ?? []
const messages: LLMChatMessage[] = [ const { messages, separateSystemMessage } = await this._convertToLLMMessagesService.prepareLLMChatMessages({
{ role: 'system', content: systemMessage }, chatMessages,
...llmMessages modelSelection,
] chatMode
})
const llmCancelToken = this._llmMessageService.sendLLMMessage({ const llmCancelToken = this._llmMessageService.sendLLMMessage({
messagesType: 'chatMessages', messagesType: 'chatMessages',
chatMode, chatMode,
messages, messages: messages,
modelSelection, modelSelection,
modelSelectionOptions, modelSelectionOptions,
logging: { loggingName: `Chat - ${chatMode}`, loggingExtras: { threadId, nMessagesSent, chatMode } }, logging: { loggingName: `Chat - ${chatMode}`, loggingExtras: { threadId, nMessagesSent, chatMode } },
separateSystemMessage: separateSystemMessage,
onText: ({ fullText, fullReasoning, toolCall }) => { onText: ({ fullText, fullReasoning, toolCall }) => {
this._setStreamState(threadId, { displayContentSoFar: fullText, reasoningSoFar: fullReasoning, toolCallSoFar: toolCall }, 'merge') this._setStreamState(threadId, { displayContentSoFar: fullText, reasoningSoFar: fullReasoning, toolCallSoFar: toolCall }, 'merge')
}, },
onFinalMessage: async ({ fullText, fullReasoning, toolCall, anthropicReasoning, }) => { 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') this._setStreamState(threadId, { displayContentSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined, toolCallSoFar: undefined }, 'merge')
resMessageIsDonePromise(toolCall) // resolve with tool calls resMessageIsDonePromise(toolCall) // resolve with tool calls
}, },
onError: (error) => { onError: (error) => {
const messageSoFar = this.streamState[threadId]?.displayContentSoFar ?? '' const messageSoFar = this.streamState[threadId]?.displayContentSoFar ?? ''
const reasoningSoFar = this.streamState[threadId]?.reasoningSoFar ?? '' 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 // 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') this._setStreamState(threadId, { error }, 'set')
resMessageIsDonePromise() resMessageIsDonePromise()
}, },
@ -742,7 +711,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
// call tool if there is one // call tool if there is one
const tool: RawToolCallObj | undefined = toolCall const tool: RawToolCallObj | undefined = toolCall
if (tool) { 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. // 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 // just detect tool interruption which is the same as chat interruption right now

View file

@ -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": "<thinking>I need to call the get_weather function, and the user wants SF, which is likely San Francisco, CA.</thinking>"
}, {
"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</${c.name}_result>`
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<number>()
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: `<SYSTEM_MESSAGE>\n${newSystemMessage}\n</SYSTEM_MESSAGE>\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<IConvertToLLMMessageService>('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"
}
}
}
*/

View file

@ -47,6 +47,7 @@ import { IVoidModelService } from '../common/voidModelService.js';
import { deepClone } from '../../../../base/common/objects.js'; import { deepClone } from '../../../../base/common/objects.js';
import { acceptBg, acceptBorder, buttonFontSize, buttonTextColor, rejectBg, rejectBorder } from '../common/helpers/colors.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 { DiffArea, Diff, CtrlKZone, VoidFileSnapshot, DiffAreaSnapshotEntry, diffAreaSnapshotKeys, DiffZone, TrackingZone, ComputedDiff } from '../common/editCodeServiceTypes.js';
import { IConvertToLLMMessageService } from './convertToLLMMessageService.js';
const configOfBG = (color: Color) => { const configOfBG = (color: Color) => {
return { dark: color, light: color, hcDark: color, hcLight: 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, @IVoidSettingsService private readonly _settingsService: IVoidSettingsService,
// @IFileService private readonly _fileService: IFileService, // @IFileService private readonly _fileService: IFileService,
@IVoidModelService private readonly _voidModelService: IVoidModelService, @IVoidModelService private readonly _voidModelService: IVoidModelService,
@IConvertToLLMMessageService private readonly _convertToLLMMessageService: IConvertToLLMMessageService,
) { ) {
super(); super();
@ -1267,6 +1269,10 @@ class EditCodeService extends Disposable implements IEditCodeService {
private _initializeWriteoverStream(opts: StartApplyingOpts): [DiffZone, Promise<void>] | undefined { private _initializeWriteoverStream(opts: StartApplyingOpts): [DiffZone, Promise<void>] | undefined {
const { from, } = opts 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) const uri = this._getURIBeforeStartApplying(opts)
if (!uri) return 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 originalCode = startRange === 'fullFile' ? originalFileCode : originalFileCode.split('\n').slice((startRange[0] - 1), (startRange[1] - 1) + 1).join('\n')
const language = model.getLanguageId() const language = model.getLanguageId()
let messages: LLMChatMessage[] let messages: LLMChatMessage[]
let separateSystemMessage: string | undefined
if (from === 'ClickApply') { if (from === 'ClickApply') {
const userContent = rewriteCode_userMessage({ originalCode, applyStr: opts.applyStr, language }) const { messages: a, separateSystemMessage: b } = this._convertToLLMMessageService.prepareLLMSimpleMessages({
messages = [ systemMessage: rewriteCode_systemMessage,
{ role: 'system', content: rewriteCode_systemMessage, }, simpleMessages: [{ role: 'user', content: rewriteCode_userMessage({ originalCode, applyStr: opts.applyStr, language }), }],
{ role: 'user', content: userContent, } featureName,
] modelSelection,
})
messages = a
separateSystemMessage = b
} }
else if (from === 'QuickEdit') { else if (from === 'QuickEdit') {
if (!ctrlKZoneIfQuickEdit) return if (!ctrlKZoneIfQuickEdit) return
@ -1316,11 +1326,16 @@ class EditCodeService extends Disposable implements IEditCodeService {
const endLine = startRange === 'fullFile' ? model.getLineCount() : startRange[1] const endLine = startRange === 'fullFile' ? model.getLineCount() : startRange[1]
const { prefix, suffix } = voidPrefixAndSuffix({ fullFileStr: originalFileCode, startLine, endLine }) const { prefix, suffix } = voidPrefixAndSuffix({ fullFileStr: originalFileCode, startLine, endLine })
const userContent = ctrlKStream_userMessage({ selection: originalCode, instructions: instructions, prefix, suffix, fimTags: quickEditFIMTags, language }) const userContent = ctrlKStream_userMessage({ selection: originalCode, instructions: instructions, prefix, suffix, fimTags: quickEditFIMTags, language })
// type: 'messages',
messages = [ const { messages: a, separateSystemMessage: b } = this._convertToLLMMessageService.prepareLLMSimpleMessages({
{ role: 'system', content: ctrlKStream_systemMessage({ quickEditFIMTags: quickEditFIMTags }), }, systemMessage: ctrlKStream_systemMessage({ quickEditFIMTags: quickEditFIMTags }),
{ role: 'user', content: userContent, } simpleMessages: [{ role: 'user', content: userContent, }],
] featureName,
modelSelection,
})
messages = a
separateSystemMessage = b
} }
else { throw new Error(`featureName ${from} is invalid`) } 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 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 // allowed to throw errors - this is called inside a promise that handles everything
const runWriteover = async () => { const runWriteover = async () => {
let shouldSendAnotherMessage = true let shouldSendAnotherMessage = true
@ -1410,6 +1421,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
messages, messages,
modelSelection, modelSelection,
modelSelectionOptions, modelSelectionOptions,
separateSystemMessage,
chatMode: null, // not chat chatMode: null, // not chat
onText: (params) => { onText: (params) => {
const { fullText: fullText_ } = params const { fullText: fullText_ } = params
@ -1485,6 +1497,9 @@ class EditCodeService extends Disposable implements IEditCodeService {
private _initializeSearchAndReplaceStream(opts: StartApplyingOpts & { from: 'ClickApply' }): [DiffZone, Promise<void>] | undefined { private _initializeSearchAndReplaceStream(opts: StartApplyingOpts & { from: 'ClickApply' }): [DiffZone, Promise<void>] | undefined {
const { from, applyStr, } = opts 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) const uri = this._getURIBeforeStartApplying(opts)
if (!uri) return if (!uri) return
@ -1498,10 +1513,13 @@ class EditCodeService extends Disposable implements IEditCodeService {
// build messages - ask LLM to generate search/replace block text // build messages - ask LLM to generate search/replace block text
const originalFileCode = model.getValue(EndOfLinePreference.LF) const originalFileCode = model.getValue(EndOfLinePreference.LF)
const userMessageContent = searchReplace_userMessage({ originalCode: originalFileCode, applyStr: applyStr }) const userMessageContent = searchReplace_userMessage({ originalCode: originalFileCode, applyStr: applyStr })
const messages: LLMChatMessage[] = [
{ role: 'system', content: searchReplace_systemMessage }, const { messages, separateSystemMessage: separateSystemMessage } = this._convertToLLMMessageService.prepareLLMSimpleMessages({
{ role: 'user', content: userMessageContent }, 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 URI is already streaming, return (should never happen, caller is responsible for checking)
if (this._uriIsStreaming(uri)) return if (this._uriIsStreaming(uri)) return
@ -1593,10 +1611,6 @@ class EditCodeService extends Disposable implements IEditCodeService {
const addedTrackingZoneOfBlockNum: TrackingZone<SearchReplaceDiffAreaMetadata>[] = [] const addedTrackingZoneOfBlockNum: TrackingZone<SearchReplaceDiffAreaMetadata>[] = []
diffZone._streamState.line = 1 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 const N_RETRIES = 2
// allowed to throw errors - this is called inside a promise that handles everything // allowed to throw errors - this is called inside a promise that handles everything
@ -1628,6 +1642,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
messages, messages,
modelSelection, modelSelection,
modelSelectionOptions, modelSelectionOptions,
separateSystemMessage,
chatMode: null, // not chat chatMode: null, // not chat
onText: (params) => { onText: (params) => {
const { fullText } = params const { fullText } = params
@ -1682,7 +1697,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
console.log('---------') console.log('---------')
const content = errContentOfInvalidStr(originalBounds, block.orig, blockNum, blocks) const content = errContentOfInvalidStr(originalBounds, block.orig, blockNum, blocks)
messages.push( 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 { role: 'user', content: content } // user explanation of what's wrong
) )

View file

@ -57,7 +57,7 @@ const CodespanWithLink = ({ text, rawText, chatMessageLocation }: { text: string
const [didComputeCodespanLink, setDidComputeCodespanLink] = useState<boolean>(false) const [didComputeCodespanLink, setDidComputeCodespanLink] = useState<boolean>(false)
let link = undefined let link = undefined
if (rawText.endsWith("`")) { // if codespan was completed if (rawText.endsWith('`')) { // if codespan was completed
// get link from cache // get link from cache
link = chatThreadService.getCodespanLink({ codespanStr: text, messageIdx, threadId }) link = chatThreadService.getCodespanLink({ codespanStr: text, messageIdx, threadId })
@ -120,11 +120,11 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
return null; return null;
} }
if (t.type === "space") { if (t.type === 'space') {
return <span>{t.raw}</span> return <span>{t.raw}</span>
} }
if (t.type === "code") { if (t.type === 'code') {
const [firstLine, remainingContents] = separateOutFirstLine(t.text) const [firstLine, remainingContents] = separateOutFirstLine(t.text)
const firstLineIsURI = isValidUri(firstLine) && !codeURI const firstLineIsURI = isValidUri(firstLine) && !codeURI
const contents = firstLineIsURI ? (remainingContents?.trimStart() || '') : t.text // exclude first-line URI from contents 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) { 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({ const applyBoxId = getApplyBoxId({
threadId: chatMessageLocation.threadId, 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 const HeadingTag = `h${t.depth}` as keyof JSX.IntrinsicElements
@ -188,7 +188,7 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
</HeadingTag> </HeadingTag>
} }
if (t.type === "table") { if (t.type === 'table') {
return ( return (
<div> <div>
<table> <table>
@ -217,14 +217,14 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
) )
// return ( // return (
// <div> // <div>
// <table className={"min-w-full border border-void-bg-2"}> // <table className={'min-w-full border border-void-bg-2'}>
// <thead> // <thead>
// <tr className="bg-void-bg-1"> // <tr className='bg-void-bg-1'>
// {t.header.map((cell: any, index: number) => ( // {t.header.map((cell: any, index: number) => (
// <th // <th
// key={index} // key={index}
// className="px-4 py-2 border border-void-bg-2 font-semibold" // className='px-4 py-2 border border-void-bg-2 font-semibold'
// style={{ textAlign: t.align[index] || "left" }} // style={{ textAlign: t.align[index] || 'left' }}
// > // >
// {cell.raw} // {cell.raw}
// </th> // </th>
@ -237,8 +237,8 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
// {row.map((cell: any, cellIndex: number) => ( // {row.map((cell: any, cellIndex: number) => (
// <td // <td
// key={cellIndex} // key={cellIndex}
// className={"px-4 py-2 border border-void-bg-2"} // className={'px-4 py-2 border border-void-bg-2'}
// style={{ textAlign: t.align[cellIndex] || "left" }} // style={{ textAlign: t.align[cellIndex] || 'left' }}
// > // >
// {cell.raw} // {cell.raw}
// </td> // </td>
@ -251,32 +251,32 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
// ) // )
} }
if (t.type === "hr") { if (t.type === 'hr') {
return <hr /> return <hr />
} }
if (t.type === "blockquote") { if (t.type === 'blockquote') {
return <blockquote>{t.text}</blockquote> return <blockquote>{t.text}</blockquote>
} }
if (t.type === 'list_item') { if (t.type === 'list_item') {
return <li> return <li>
<input type="checkbox" checked={t.checked} readOnly /> <input type='checkbox' checked={t.checked} readOnly />
<span> <span>
<ChatMarkdownRender chatMessageLocation={chatMessageLocation} string={t.text} inPTag={true} codeURI={codeURI} {...options} /> <ChatMarkdownRender chatMessageLocation={chatMessageLocation} string={t.text} inPTag={true} codeURI={codeURI} {...options} />
</span> </span>
</li> </li>
} }
if (t.type === "list") { if (t.type === 'list') {
const ListTag = t.ordered ? "ol" : "ul" const ListTag = t.ordered ? 'ol' : 'ul'
return ( return (
<ListTag start={t.start ? t.start : undefined}> <ListTag start={t.start ? t.start : undefined}>
{t.items.map((item, index) => ( {t.items.map((item, index) => (
<li key={index}> <li key={index}>
{item.task && ( {item.task && (
<input type="checkbox" checked={item.checked} readOnly /> <input type='checkbox' checked={item.checked} readOnly />
)} )}
<span> <span>
<ChatMarkdownRender chatMessageLocation={chatMessageLocation} string={item.text} inPTag={true} {...options} /> <ChatMarkdownRender chatMessageLocation={chatMessageLocation} string={item.text} inPTag={true} {...options} />
@ -287,7 +287,7 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
) )
} }
if (t.type === "paragraph") { if (t.type === 'paragraph') {
const contents = <> const contents = <>
{t.tokens.map((token, index) => ( {t.tokens.map((token, index) => (
<RenderToken key={index} <RenderToken key={index}
@ -304,15 +304,15 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
return <p>{contents}</p> return <p>{contents}</p>
} }
if (t.type === "text" || t.type === "escape") { if (t.type === 'text' || t.type === 'escape' || t.type === 'html') {
return <span>{t.raw}</span> return <span>{t.raw}</span>
} }
if (t.type === "def") { if (t.type === 'def') {
return <></> // Definitions are typically not rendered return <></> // Definitions are typically not rendered
} }
if (t.type === "link") { if (t.type === 'link') {
return ( return (
<a <a
onClick={() => { window.open(t.href) }} onClick={() => { window.open(t.href) }}
@ -325,7 +325,7 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
) )
} }
if (t.type === "image") { if (t.type === 'image') {
return <img return <img
src={t.href} src={t.href}
alt={t.text} alt={t.text}
@ -334,16 +334,16 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
/> />
} }
if (t.type === "strong") { if (t.type === 'strong') {
return <strong>{t.text}</strong> return <strong>{t.text}</strong>
} }
if (t.type === "em") { if (t.type === 'em') {
return <em>{t.text}</em> return <em>{t.text}</em>
} }
// inline code // inline code
if (t.type === "codespan" || t.type === "html") { if (t.type === 'codespan') {
if (options.isLinkDetectionEnabled && chatMessageLocation) { if (options.isLinkDetectionEnabled && chatMessageLocation) {
return <CodespanWithLink return <CodespanWithLink
@ -357,18 +357,18 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
return <Codespan text={t.text} /> return <Codespan text={t.text} />
} }
if (t.type === "br") { if (t.type === 'br') {
return <br /> return <br />
} }
// strikethrough // strikethrough
if (t.type === "del") { if (t.type === 'del') {
return <del>{t.text}</del> return <del>{t.text}</del>
} }
// default // default
return ( return (
<div className="bg-orange-50 rounded-sm overflow-hidden p-2"> <div className='bg-orange-50 rounded-sm overflow-hidden p-2'>
<span className="text-sm text-orange-500">Unknown token rendered...</span> <span className='text-sm text-orange-500'>Unknown token rendered...</span>
</div> </div>
) )
} }

View file

@ -29,6 +29,7 @@ import { acceptAllBg, acceptBorder, buttonFontSize, buttonTextColor, rejectAllBg
import { ToolName, toolNames } from '../../../../common/prompt/prompts.js'; import { ToolName, toolNames } from '../../../../common/prompt/prompts.js';
import { error } from 'console'; import { error } from 'console';
import { RawToolCallObj } from '../../../../common/sendLLMMessageTypes.js'; import { RawToolCallObj } from '../../../../common/sendLLMMessageTypes.js';
import { MAX_FILE_CHARS_PAGE } from '../../../toolsService.js';
@ -1298,7 +1299,7 @@ const ToolRequestAcceptRejectButtons = () => {
</div> </div>
) )
return <div className="flex gap-2 my-1 items-center"> return <div className="flex gap-2 mx-4 items-center">
{approveButton} {approveButton}
{cancelButton} {cancelButton}
{autoApproveToggle} {autoApproveToggle}
@ -1434,17 +1435,17 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
const componentParams: ToolHeaderParams = { title, desc1, isError, icon } const componentParams: ToolHeaderParams = { title, desc1, isError, icon }
if (toolMessage.params.startLine !== null || toolMessage.params.endLine !== null) { if (toolMessage.params.startLine !== null || toolMessage.params.endLine !== null) {
const start = toolMessage.params.startLine === null ? `start` : `${toolMessage.params.startLine}` const start = toolMessage.params.startLine === null ? `1` : `${toolMessage.params.startLine}`
const end = toolMessage.params.endLine === null ? `end` : `${toolMessage.params.endLine}` const end = toolMessage.params.endLine === null ? `` : `${toolMessage.params.endLine}`
const addStr = `(${start}-${end})` const addStr = `(${start}-${end})`
componentParams.title += ` ${addStr}` componentParams.desc1 += ` ${addStr}`
} }
if (toolMessage.type === 'success') { if (toolMessage.type === 'success') {
const { params, result } = toolMessage const { params, result } = toolMessage
componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) } componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }
if (result.hasNextPage && params.pageNumber === 1) // first page if (result.hasNextPage && params.pageNumber === 1) // first page
componentParams.desc2 = '(more content available)' componentParams.desc2 = `(first ${Math.round(MAX_FILE_CHARS_PAGE) / 1000}k)`
else if (params.pageNumber > 1) // subsequent pages else if (params.pageNumber > 1) // subsequent pages
componentParams.desc2 = `(part ${params.pageNumber})` componentParams.desc2 = `(part ${params.pageNumber})`
} }
@ -2492,7 +2493,6 @@ export const SidebarChat = () => {
role: 'assistant', role: 'assistant',
displayContent: displayContentSoFar ?? '', displayContent: displayContentSoFar ?? '',
reasoning: reasoningSoFar ?? '', reasoning: reasoningSoFar ?? '',
toolCall: toolCallSoFar,
anthropicReasoning: null, anthropicReasoning: null,
}} }}
messageIdx={streamingChatIdx} messageIdx={streamingChatIdx}

View file

@ -312,12 +312,11 @@ export const ModelDump = () => {
</div> </div>
{/* right part is anything that fits */} {/* right part is anything that fits */}
<div className='flex items-center gap-4' <div className='flex items-center gap-4'
data-tooltip-id='void-tooltip' // data-tooltip-id='void-tooltip'
data-tooltip-place='top' // data-tooltip-place='top'
data-tooltip-content={disabled? `${displayInfoOfProviderName(providerName).title} is disabled` // data-tooltip-content={disabled ? `${displayInfoOfProviderName(providerName).title} is disabled`
: (isHidden ? `'${modelName}' won't appear in dropdowns` : ``) // : (isHidden ? `'${modelName}' won't appear in dropdowns` : ``)
// }
}
> >
<span className='opacity-50 truncate'>{isAutodetected ? '(detected locally)' : isDefault ? '' : '(custom model)'}</span> <span className='opacity-50 truncate'>{isAutodetected ? '(detected locally)' : isDefault ? '' : '(custom model)'}</span>
@ -616,7 +615,19 @@ export const FeaturesTab = () => {
{/* FIM */} {/* FIM */}
<div className='w-full'> <div className='w-full'>
<h4 className={`text-base`}>{displayInfoOfFeatureName('Autocomplete')}</h4> <h4 className={`text-base`}>{displayInfoOfFeatureName('Autocomplete')}</h4>
<div className='text-sm italic text-void-fg-3 mt-1 mb-4'>Experimental. Only works with models that support FIM.</div> <div className='text-sm italic text-void-fg-3 mt-1 mb-4'>
<span>
Experimental. Only works with FIM models.
</span>
<span
className='hover:brightness-110'
data-tooltip-id='void-tooltip'
data-tooltip-content='We recommend using qwen2.5-coder:1.5b with Ollama.'
data-tooltip-class-name='void-max-w-[20px]'
>
*
</span>
</div>
<div className='my-2'> <div className='my-2'>
{/* Enable Switch */} {/* Enable Switch */}
@ -696,7 +707,7 @@ export const FeaturesTab = () => {
value={voidSettingsState.globalSettings.includeToolLintErrors} value={voidSettingsState.globalSettings.includeToolLintErrors}
onChange={(newVal) => voidSettingsService.setGlobalSetting('includeToolLintErrors', newVal)} onChange={(newVal) => voidSettingsService.setGlobalSetting('includeToolLintErrors', newVal)}
/> />
<span className='text-void-fg-3 text-xs pointer-events-none'>{voidSettingsState.globalSettings.includeToolLintErrors ? 'Include after-edit lint errors' : `Don't include lint errors`}</span> <span className='text-void-fg-3 text-xs pointer-events-none'>{voidSettingsState.globalSettings.includeToolLintErrors ? 'Fix lint errors' : `Don't fix lint errors`}</span>
</div> </div>
</div> </div>
</div> </div>

View file

@ -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<ISearchReplaceService>('SearchReplaceCacheService');
export class SearchReplaceService extends Disposable implements ISearchReplaceService {
_serviceBrand: undefined;
private readonly _onDidChangeState = new Emitter<void>();
readonly onDidChangeState: Event<void> = 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);

View file

@ -29,6 +29,32 @@ import { IChatThreadService } from './chatThreadService.js';
// ---------- Register commands and keybindings ---------- // ---------- Register commands and keybindings ----------
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' }) => { export const roundRangeToLines = (range: IRange | null | undefined, options: { emptySelectionBehavior: 'null' | 'line' }) => {
if (!range) if (!range)
return null return null
@ -63,31 +89,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' const VOID_OPEN_SIDEBAR_ACTION_ID = 'void.sidebar.open'
registerAction2(class extends Action2 { registerAction2(class extends Action2 {
@ -132,7 +133,7 @@ registerAction2(class extends Action2 {
} }
const selection: StagingSelectionItem = !selectionRange || (selectionRange.startLineNumber > selectionRange.endLineNumber) ? { const newSelection: StagingSelectionItem = !selectionRange || (selectionRange.startLineNumber > selectionRange.endLineNumber) ? {
type: 'File', type: 'File',
uri: model.uri, uri: model.uri,
language: model.getLanguageId(), language: model.getLanguageId(),
@ -163,21 +164,17 @@ registerAction2(class extends Action2 {
} }
// if matches with existing selection, overwrite (since text may change) // if matches with existing selection, overwrite (since text may change)
const replaceRes = findStagingItemToReplace(selections, selection) const idx = findStagingSelectionIndex(selections, newSelection)
if (replaceRes) { if (idx !== null && idx !== -1) {
const [idx, newSel] = replaceRes setSelections([
...selections!.slice(0, idx),
if (idx !== undefined && idx !== -1) { newSelection,
setSelections([ ...selections!.slice(idx + 1, Infinity)
...selections!.slice(0, idx), ])
newSel,
...selections!.slice(idx + 1, Infinity)
])
}
} }
// if no match, add it // if no match, add it
else { else {
setSelections([...(selections ?? []), selection]) setSelections([...(selections ?? []), newSelection])
} }
} }

View file

@ -14,7 +14,7 @@ import { EndOfLinePreference } from '../../../../editor/common/model.js'
import { basename } from '../../../../base/common/path.js' import { basename } from '../../../../base/common/path.js'
import { IVoidCommandBarService } from './voidCommandBarService.js' import { IVoidCommandBarService } from './voidCommandBarService.js'
import { computeDirectoryTree1Deep, IDirectoryStrService, stringifyDirectoryTree1Deep } from './directoryStrService.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 { timeout } from '../../../../base/common/async.js'
import { RawToolParamsObj } from '../common/sendLLMMessageTypes.js' import { RawToolParamsObj } from '../common/sendLLMMessageTypes.js'
import { ToolName } from '../common/prompt/prompts.js' import { ToolName } from '../common/prompt/prompts.js'
@ -276,8 +276,8 @@ export class ToolsService implements IToolsService {
const toIdx = MAX_FILE_CHARS_PAGE * pageNumber - 1 const toIdx = MAX_FILE_CHARS_PAGE * pageNumber - 1
const fileContents = contents.slice(fromIdx, toIdx + 1) // paginate const fileContents = contents.slice(fromIdx, toIdx + 1) // paginate
const hasNextPage = (contents.length - 1) - toIdx >= 1 const hasNextPage = (contents.length - 1) - toIdx >= 1
const totalFileLen = contents.length
return { result: { fileContents, hasNextPage } } return { result: { fileContents, totalFileLen, hasNextPage } }
}, },
ls_dir: async ({ rootURI, pageNumber }) => { ls_dir: async ({ rootURI, pageNumber }) => {
@ -400,7 +400,7 @@ export class ToolsService implements IToolsService {
// given to the LLM after the call // given to the LLM after the call
this.stringOfResult = { this.stringOfResult = {
read_file: (params, result) => { 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) => { ls_dir: (params, result) => {
const dirTreeStr = stringifyDirectoryTree1Deep(params, result) const dirTreeStr = stringifyDirectoryTree1Deep(params, result)
@ -471,9 +471,10 @@ export class ToolsService implements IToolsService {
private _getLintErrors(uri: URI): { lintErrors: LintErrorItem[] | null } { private _getLintErrors(uri: URI): { lintErrors: LintErrorItem[] | null } {
const lintErrors = this.markerService const lintErrors = this.markerService
.read({ resource: uri }) .read({ resource: uri })
.filter(l => l.severity === MarkerSeverity.Error || l.severity === MarkerSeverity.Warning)
.map(l => ({ .map(l => ({
code: typeof l.code === 'string' ? l.code : l.code?.value || '', 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, startLineNumber: l.startLineNumber,
endLineNumber: l.endLineNumber, endLineNumber: l.endLineNumber,
} satisfies LintErrorItem)) } satisfies LintErrorItem))

View file

@ -6,15 +6,17 @@
import { URI } from '../../../../base/common/uri.js'; import { URI } from '../../../../base/common/uri.js';
import { VoidFileSnapshot } from './editCodeServiceTypes.js'; import { VoidFileSnapshot } from './editCodeServiceTypes.js';
import { ToolName } from './prompt/prompts.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'; import { ToolCallParams, ToolResultType } from './toolsServiceTypes.js';
export type ToolMessage<T extends ToolName> = { export type ToolMessage<T extends ToolName> = {
role: 'tool'; role: 'tool';
content: string; // give this result to LLM (string of value) content: string; // give this result to LLM (string of value)
id: string;
rawParams: RawToolParamsObj;
} & ( } & (
// in order of events: // 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 | { type: 'tool_request', result: null, name: T, params: ToolCallParams[T], } // params were validated, awaiting user
@ -22,7 +24,7 @@ export type ToolMessage<T extends ToolName> = {
| { type: 'tool_error', result: string, name: T, params: ToolCallParams[T], } // error when tool was running | { type: 'tool_error', result: string, name: T, params: ToolCallParams[T], } // error when tool was running
| { type: 'success', result: Awaited<ToolResultType[T]>, name: T, params: ToolCallParams[T], } | { type: 'success', result: Awaited<ToolResultType[T]>, 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 ) // user rejected
export type DecorativeCanceledTool = { export type DecorativeCanceledTool = {
@ -58,7 +60,6 @@ export type ChatMessage =
role: 'assistant'; role: 'assistant';
displayContent: string; // content received from LLM - allowed to be '', will be replaced with (empty) 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 reasoning: string; // reasoning from the LLM, used for step-by-step thinking
toolCall: RawToolCallObj | undefined;
anthropicReasoning: AnthropicReasoning[] | null; // anthropic reasoning anthropicReasoning: AnthropicReasoning[] | null; // anthropic reasoning
} }

View file

@ -130,6 +130,7 @@ export type VoidStaticModelInfo = { // not stateful
} }
supportsSystemMessage: false | 'system-role' | 'developer-role' | 'separated'; // separated = anthropic where "system" is a special paramete 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; supportsFIM: boolean;
reasoningCapabilities: false | { reasoningCapabilities: false | {
@ -377,6 +378,7 @@ const anthropicModelOptions = {
cost: { input: 3.00, cache_read: 0.30, cache_write: 3.75, output: 15.00 }, cost: { input: 3.00, cache_read: 0.30, cache_write: 3.75, output: 15.00 },
downloadable: false, downloadable: false,
supportsFIM: false, supportsFIM: false,
specialToolFormat: 'anthropic-style',
supportsSystemMessage: 'separated', supportsSystemMessage: 'separated',
reasoningCapabilities: { reasoningCapabilities: {
supportsReasoning: true, 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 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 reasoningBudgetSlider: { type: 'slider', min: 1024, max: 32_000, default: 1024 }, // they recommend batching if max > 32_000
}, },
}, },
'claude-3-5-sonnet-20241022': { 'claude-3-5-sonnet-20241022': {
contextWindow: 200_000, contextWindow: 200_000,
@ -392,6 +395,7 @@ const anthropicModelOptions = {
cost: { input: 3.00, cache_read: 0.30, cache_write: 3.75, output: 15.00 }, cost: { input: 3.00, cache_read: 0.30, cache_write: 3.75, output: 15.00 },
downloadable: false, downloadable: false,
supportsFIM: false, supportsFIM: false,
specialToolFormat: 'anthropic-style',
supportsSystemMessage: 'separated', supportsSystemMessage: 'separated',
reasoningCapabilities: false, reasoningCapabilities: false,
}, },
@ -401,6 +405,7 @@ const anthropicModelOptions = {
cost: { input: 0.80, cache_read: 0.08, cache_write: 1.00, output: 4.00 }, cost: { input: 0.80, cache_read: 0.08, cache_write: 1.00, output: 4.00 },
downloadable: false, downloadable: false,
supportsFIM: false, supportsFIM: false,
specialToolFormat: 'anthropic-style',
supportsSystemMessage: 'separated', supportsSystemMessage: 'separated',
reasoningCapabilities: false, reasoningCapabilities: false,
}, },
@ -410,6 +415,7 @@ const anthropicModelOptions = {
cost: { input: 15.00, cache_read: 1.50, cache_write: 18.75, output: 75.00 }, cost: { input: 15.00, cache_read: 1.50, cache_write: 18.75, output: 75.00 },
downloadable: false, downloadable: false,
supportsFIM: false, supportsFIM: false,
specialToolFormat: 'anthropic-style',
supportsSystemMessage: 'separated', supportsSystemMessage: 'separated',
reasoningCapabilities: false, reasoningCapabilities: false,
}, },
@ -418,6 +424,7 @@ const anthropicModelOptions = {
downloadable: false, downloadable: false,
maxOutputTokens: 4_096, maxOutputTokens: 4_096,
supportsFIM: false, supportsFIM: false,
specialToolFormat: 'anthropic-style',
supportsSystemMessage: 'separated', supportsSystemMessage: 'separated',
reasoningCapabilities: false, 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 }, cost: { input: 2.00, output: 8.00, cache_read: 0.50 },
downloadable: false, downloadable: false,
supportsFIM: false, supportsFIM: false,
specialToolFormat: 'openai-style',
supportsSystemMessage: 'developer-role', supportsSystemMessage: 'developer-role',
reasoningCapabilities: false, 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 }, cost: { input: 0.40, output: 1.60, cache_read: 0.10 },
downloadable: false, downloadable: false,
supportsFIM: false, supportsFIM: false,
specialToolFormat: 'openai-style',
supportsSystemMessage: 'developer-role', supportsSystemMessage: 'developer-role',
reasoningCapabilities: false, 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 }, cost: { input: 0.10, output: 0.40, cache_read: 0.03 },
downloadable: false, downloadable: false,
supportsFIM: false, supportsFIM: false,
specialToolFormat: 'openai-style',
supportsSystemMessage: 'developer-role', supportsSystemMessage: 'developer-role',
reasoningCapabilities: false, 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, }, cost: { input: 2.50, cache_read: 1.25, output: 10.00, },
downloadable: false, downloadable: false,
supportsFIM: false, supportsFIM: false,
specialToolFormat: 'openai-style',
supportsSystemMessage: 'system-role', supportsSystemMessage: 'system-role',
reasoningCapabilities: false, 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, }, cost: { input: 0.15, cache_read: 0.075, output: 0.60, },
downloadable: false, downloadable: false,
supportsFIM: false, supportsFIM: false,
specialToolFormat: 'openai-style',
supportsSystemMessage: 'system-role', // ?? supportsSystemMessage: 'system-role', // ??
reasoningCapabilities: false, reasoningCapabilities: false,
}, },
@ -550,6 +562,15 @@ const xAIModelOptions = {
supportsSystemMessage: 'system-role', supportsSystemMessage: 'system-role',
reasoningCapabilities: false, 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 } } as const satisfies { [s: string]: VoidStaticModelInfo }
const xAISettings: VoidStaticProviderInfo = { 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 // used to force reasoning state (complex) into something simple we can just read from when sending a message
export const getSendableReasoningInfo = ( export const getSendableReasoningInfo = (
featureName: FeatureName, featureName: FeatureName,

View file

@ -6,7 +6,7 @@
import { EndOfLinePreference } from '../../../../../editor/common/model.js'; import { EndOfLinePreference } from '../../../../../editor/common/model.js';
import { StagingSelectionItem } from '../chatThreadServiceTypes.js'; import { StagingSelectionItem } from '../chatThreadServiceTypes.js';
import { os } from '../helpers/systemInfo.js'; import { os } from '../helpers/systemInfo.js';
import { RawToolCallObj } from '../sendLLMMessageTypes.js'; import { RawToolParamsObj } from '../sendLLMMessageTypes.js';
import { toolNamesThatRequireApproval } from '../toolsServiceTypes.js'; import { toolNamesThatRequireApproval } from '../toolsServiceTypes.js';
import { IVoidModelService } from '../voidModelService.js'; import { IVoidModelService } from '../voidModelService.js';
import { ChatMode } from '../voidSettingsTypes.js'; import { ChatMode } from '../voidSettingsTypes.js';
@ -218,12 +218,11 @@ Format:
}).join('\n\n')}` }).join('\n\n')}`
} }
export const toolCallXMLStr = (toolCall: RawToolCallObj) => { export const toolCallXMLStr = (toolName: ToolName, toolParams: RawToolParamsObj) => {
const t = toolCall const params = Object.keys(toolParams).map(paramName => `<${paramName}>${toolParams[paramName as ToolParamName]}</${paramName}>`).join('\n')
const params = Object.keys(t.rawParams).map(paramName => `<${paramName}>${t.rawParams[paramName as ToolParamName]}</${paramName}>`).join('\n')
return `\ return `\
<${toolCall.name}>${!params ? '' : `\n${params}`} <${toolName}>${!params ? '' : `\n${params}`}
</${toolCall.name}>` </${toolName}>`
.replace('\t', ' ') .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. // - 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 systemToolsXMLPrompt = (chatMode: ChatMode) => {
const tools = availableTools(chatMode) const tools = availableTools(chatMode)
if (!tools || tools.length === 0) return '' if (!tools || tools.length === 0) return null
const toolXMLDefinitions = (`\ const toolXMLDefinitions = (`\
Available tools: Available tools:
@ -255,7 +254,7 @@ ${toolCallXMLGuidelines}`
// ======================================================== chat (normal, gather, agent) ======================================================== // ======================================================== 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 \ 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 === '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.` : mode === 'gather' ? `to search, understand, and reference files in the user's codebase.`
@ -289,7 +288,7 @@ ${directoryStr}
</files_overview>`) </files_overview>`)
const toolDefinitions = systemToolsXMLPrompt(mode) const toolDefinitions = includeXMLToolDefinitions ? systemToolsXMLPrompt(mode) : null
const details: string[] = [] const details: string[] = []
@ -630,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 ======================================================== // ======================================================== ai search/replace ========================================================

View file

@ -13,7 +13,6 @@ import { generateUuid } from '../../../../base/common/uuid.js';
import { Event } from '../../../../base/common/event.js'; import { Event } from '../../../../base/common/event.js';
import { Disposable } from '../../../../base/common/lifecycle.js'; import { Disposable } from '../../../../base/common/lifecycle.js';
import { IVoidSettingsService } from './voidSettingsService.js'; import { IVoidSettingsService } from './voidSettingsService.js';
// import { INotificationService } from '../../notification/common/notification.js';
// calls channel to implement features // calls channel to implement features
export const ILLMMessageService = createDecorator<ILLMMessageService>('llmMessageService'); export const ILLMMessageService = createDecorator<ILLMMessageService>('llmMessageService');
@ -98,6 +97,7 @@ export class LLMMessageService extends Disposable implements ILLMMessageService
return null return null
} }
const { settingsOfProvider, } = this.voidSettingsService.state
// add state for request id // add state for request id
const requestId = generateUuid(); const requestId = generateUuid();
@ -106,13 +106,9 @@ export class LLMMessageService extends Disposable implements ILLMMessageService
this.llmMessageHooks.onError[requestId] = onError this.llmMessageHooks.onError[requestId] = onError
this.llmMessageHooks.onAbort[requestId] = onAbort // used internally only 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 // params will be stripped of all its functions over the IPC channel
this.channel.call('sendLLMMessage', { this.channel.call('sendLLMMessage', {
...proxyParams, ...proxyParams,
aiInstructions,
requestId, requestId,
settingsOfProvider, settingsOfProvider,
modelSelection, modelSelection,

View file

@ -27,16 +27,39 @@ export const getErrorMessage: (error: unknown) => string = (error) => {
} }
export type LLMChatMessage = {
role: 'system'; export type AnthropicLLMChatMessage = {
content: string; role: 'assistant',
content: string | (AnthropicReasoning | { type: 'text'; text: string }
| { type: 'tool_use'; name: string; input: Record<string, any>; 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; content: string;
} | { } | {
role: 'assistant', role: 'assistant',
content: string; // text content content: string | (AnthropicReasoning | { type: 'text'; text: string })[];
anthropicReasoning: AnthropicReasoning[] | null; 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; name: ToolName;
rawParams: RawToolParamsObj; rawParams: RawToolParamsObj;
doneParams: ToolParamName[]; doneParams: ToolParamName[];
id: string;
isDone: boolean; isDone: boolean;
}; };
export type AnthropicReasoning = ({ type: 'thinking'; thinking: any; signature: string; } | { type: 'redacted_thinking', data: any }) 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 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 AbortRef = { current: (() => void) | null }
export type LLMFIMMessage = { // service types
prefix: string;
suffix: string;
stopTokens: string[];
}
type SendLLMType = { type SendLLMType = {
messagesType: 'chatMessages'; 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; chatMode: ChatMode | null;
} | { } | {
messagesType: 'FIMMessage'; messagesType: 'FIMMessage';
messages: LLMFIMMessage; messages: LLMFIMMessage;
separateSystemMessage?: undefined;
chatMode?: undefined; chatMode?: undefined;
} }
// service types
export type ServiceSendLLMMessageParams = { export type ServiceSendLLMMessageParams = {
onText: OnText; onText: OnText;
onFinalMessage: OnFinalMessage; onFinalMessage: OnFinalMessage;
@ -95,8 +113,6 @@ export type SendLLMMessageParams = {
logging: { loggingName: string, loggingExtras?: { [k: string]: any } }; logging: { loggingName: string, loggingExtras?: { [k: string]: any } };
abortRef: AbortRef; abortRef: AbortRef;
aiInstructions: string;
modelSelection: ModelSelection; modelSelection: ModelSelection;
modelSelectionOptions: ModelSelectionOptions | undefined; modelSelectionOptions: ModelSelectionOptions | undefined;

View file

@ -39,7 +39,7 @@ export type ToolCallParams = {
// RESULT OF TOOL CALL // RESULT OF TOOL CALL
export type ToolResultType = { 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 }, 'ls_dir': { children: ShallowDirectoryItem[] | null, hasNextPage: boolean, hasPrevPage: boolean, itemsRemaining: number },
'get_dir_structure': { str: string, }, 'get_dir_structure': { str: string, },
'search_pathnames_only': { uris: URI[], hasNextPage: boolean }, 'search_pathnames_only': { uris: URI[], hasNextPage: boolean },

View file

@ -3,6 +3,7 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. * 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 { endsWithAnyPrefixOf, SurroundingsRemover } from '../../common/helpers/extractCodeFromResult.js'
import { availableTools, InternalToolInfo, ToolName, ToolParamName } from '../../common/prompt/prompts.js' import { availableTools, InternalToolInfo, ToolName, ToolParamName } from '../../common/prompt/prompts.js'
import { OnFinalMessage, OnText, RawToolCallObj, RawToolParamsObj } from '../../common/sendLLMMessageTypes.js' import { OnFinalMessage, OnText, RawToolCallObj, RawToolParamsObj } from '../../common/sendLLMMessageTypes.js'
@ -134,7 +135,7 @@ export const extractReasoningWrapper = (
} }
// =============== tools =============== // =============== tools (XML) ===============
@ -160,7 +161,7 @@ const findIndexOfAny = (fullText: string, matches: string[]) => {
type ToolOfToolName = { [toolName: string]: InternalToolInfo | undefined } 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 paramsObj: RawToolParamsObj = {}
const doneParams: ToolParamName[] = [] const doneParams: ToolParamName[] = []
let isDone = false let isDone = false
@ -179,7 +180,8 @@ const parseXMLPrefixToToolCall = (toolName: ToolName, str: string, toolOfToolNam
name: toolName, name: toolName,
rawParams: paramsObj, rawParams: paramsObj,
doneParams: doneParams, doneParams: doneParams,
isDone: isDone isDone: isDone,
id: toolId,
} }
return ans return ans
} }
@ -254,9 +256,11 @@ const parseXMLPrefixToToolCall = (toolName: ToolName, str: string, toolOfToolNam
} }
} }
export const extractToolsWrapper = ( export const extractXMLToolsWrapper = (
onText: OnText, onFinalMessage: OnFinalMessage, chatMode: ChatMode onText: OnText, onFinalMessage: OnFinalMessage, chatMode: ChatMode | null
): { newOnText: OnText, newOnFinalMessage: OnFinalMessage } => { ): { newOnText: OnText, newOnFinalMessage: OnFinalMessage } => {
if (!chatMode) return { newOnText: onText, newOnFinalMessage: onFinalMessage }
const tools = availableTools(chatMode) const tools = availableTools(chatMode)
if (!tools) return { newOnText: onText, newOnFinalMessage: onFinalMessage } if (!tools) return { newOnText: onText, newOnFinalMessage: onFinalMessage }
@ -264,6 +268,8 @@ export const extractToolsWrapper = (
const toolOpenTags = tools.map(t => `<${t.name}>`) const toolOpenTags = tools.map(t => `<${t.name}>`)
for (const t of tools) { toolOfToolName[t.name] = t } for (const t of tools) { toolOfToolName[t.name] = t }
const toolId = generateUuid()
// detect <availableTools[0]></availableTools[0]>, etc // detect <availableTools[0]></availableTools[0]>, etc
let fullText = ''; let fullText = '';
let trueFullText = '' let trueFullText = ''
@ -315,14 +321,12 @@ export const extractToolsWrapper = (
if (foundOpenTag !== null) { if (foundOpenTag !== null) {
latestToolCall = parseXMLPrefixToToolCall( latestToolCall = parseXMLPrefixToToolCall(
foundOpenTag.toolName, foundOpenTag.toolName,
toolId,
trueFullText.substring(foundOpenTag.idx, Infinity), trueFullText.substring(foundOpenTag.idx, Infinity),
toolOfToolName, toolOfToolName,
) )
} }
onText({ onText({
...params, ...params,
fullText, fullText,

View file

@ -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<number>()
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: (''
+ '<SYSTEM_MESSAGE>\n'
+ systemMessageStr
+ '\n'
+ '</SYSTEM_MESSAGE>\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<InternalLLMChatMessage, { role: 'assistant' | 'tool' }> | {
// 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": "<thinking>I need to call the get_weather function, and the user wants SF, which is likely San Francisco, CA.</thinking>"
// }, {
// "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<InternalLLMChatMessage, { role: 'assistant' | 'user' }> | {
// role: 'assistant',
// content: string | (
// | AnthropicReasoning
// | {
// type: 'text';
// text: string;
// }
// | {
// type: 'tool_use';
// name: string;
// input: Record<string, any>;
// 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"
}
}
}
*/

View file

@ -10,15 +10,14 @@ import { MistralCore } from '@mistralai/mistralai/core.js';
import { fimComplete } from '@mistralai/mistralai/funcs/fimComplete.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, RawToolCallObj, RawToolParamsObj } from '../../common/sendLLMMessageTypes.js';
import { ChatMode, displayInfoOfProviderName, ModelSelectionOptions, ProviderName, SettingsOfProvider } from '../../common/voidSettingsTypes.js'; import { ChatMode, displayInfoOfProviderName, ModelSelectionOptions, ProviderName, SettingsOfProvider } from '../../common/voidSettingsTypes.js';
import { prepareFIMMessage, prepareMessages } from './preprocessLLMMessages.js'; import { getSendableReasoningInfo, getModelCapabilities, getProviderCapabilities, defaultProviderSettings, getMaxOutputTokens } from '../../common/modelCapabilities.js';
import { getSendableReasoningInfo, getModelCapabilities, getProviderCapabilities, defaultProviderSettings } from '../../common/modelCapabilities.js'; import { extractReasoningWrapper, extractXMLToolsWrapper } from './extractGrammar.js';
import { extractReasoningWrapper, extractToolsWrapper } from './extractGrammar.js'; import { availableTools, InternalToolInfo, isAToolName, ToolParamName, voidTools } from '../../common/prompt/prompts.js';
type InternalCommonMessageParams = { type InternalCommonMessageParams = {
aiInstructions: string;
onText: OnText; onText: OnText;
onFinalMessage: OnFinalMessage; onFinalMessage: OnFinalMessage;
onError: OnError; onError: OnError;
@ -29,8 +28,8 @@ type InternalCommonMessageParams = {
_setAborter: (aborter: () => void) => void; _setAborter: (aborter: () => void) => void;
} }
type SendChatParams_Internal = InternalCommonMessageParams & { messages: LLMChatMessage[]; chatMode: ChatMode | null; } type SendChatParams_Internal = InternalCommonMessageParams & { messages: LLMChatMessage[]; separateSystemMessage: string | undefined; chatMode: ChatMode | null; }
type SendFIMParams_Internal = InternalCommonMessageParams & { messages: LLMFIMMessage; } type SendFIMParams_Internal = InternalCommonMessageParams & { messages: LLMFIMMessage; separateSystemMessage: string | undefined; }
export type ListParams_Internal<ModelResponse> = ModelListParams<ModelResponse> export type ListParams_Internal<ModelResponse> = ModelListParams<ModelResponse>
@ -96,7 +95,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_) const { modelName, supportsFIM } = getModelCapabilities(providerName, modelName_)
if (!supportsFIM) { if (!supportsFIM) {
if (modelName === modelName_) if (modelName === modelName_)
@ -106,16 +105,14 @@ const _sendOpenAICompatibleFIM = ({ messages: messages_, onFinalMessage, onError
return return
} }
const messages = prepareFIMMessage({ messages: messages_, aiInstructions, })
const openai = newOpenAICompatibleSDK({ providerName, settingsOfProvider }) const openai = newOpenAICompatibleSDK({ providerName, settingsOfProvider })
openai.completions openai.completions
.create({ .create({
model: modelName, model: modelName,
prompt: messages.prefix, prompt: prefix,
suffix: messages.suffix, suffix: suffix,
stop: messages.stopTokens, stop: stopTokens,
max_tokens: messages.maxTokens, max_tokens: 300,
}) })
.then(async response => { .then(async response => {
const fullText = response.choices[0]?.text const fullText = response.choices[0]?.text
@ -128,14 +125,61 @@ const _sendOpenAICompatibleFIM = ({ messages: messages_, onFinalMessage, onError
} }
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 }
}
const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName: modelName_, _setAborter, providerName, aiInstructions, chatMode }: SendChatParams_Internal) => { // ------------ OPENAI-COMPATIBLE ------------
const _sendOpenAICompatibleChat = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName: modelName_, _setAborter, providerName, chatMode, separateSystemMessage }: SendChatParams_Internal) => {
const { const {
modelName, modelName,
supportsSystemMessage, specialToolFormat,
contextWindow,
maxOutputTokens,
reasoningCapabilities, reasoningCapabilities,
} = getModelCapabilities(providerName, modelName_) } = getModelCapabilities(providerName, modelName_)
@ -146,16 +190,17 @@ const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage
const reasoningInfo = getSendableReasoningInfo('Chat', providerName, modelName_, modelSelectionOptions) // user's modelName_ here const reasoningInfo = getSendableReasoningInfo('Chat', providerName, modelName_, modelSelectionOptions) // user's modelName_ here
const includeInPayload = providerReasoningIOSettings?.input?.includeInPayload?.(reasoningInfo) || {} const includeInPayload = providerReasoningIOSettings?.input?.includeInPayload?.(reasoningInfo) || {}
// max tokens // tools
const maxTokens = reasoningInfo?.isReasoningEnabled && reasoningCapabilities ? reasoningCapabilities.reasoningMaxOutputTokens : maxOutputTokens const potentialTools = chatMode !== null ? openAITools(chatMode) : null
const nativeToolsObj = potentialTools ? { tools: potentialTools } as const : {}
// instance // instance
const { messages } = prepareMessages({ messages: messages_, aiInstructions, supportsSystemMessage, supportsAnthropicReasoningSignature: false, contextWindow, maxOutputTokens: maxTokens })
const openai: OpenAI = newOpenAICompatibleSDK({ providerName, settingsOfProvider, includeInPayload }) const openai: OpenAI = newOpenAICompatibleSDK({ providerName, settingsOfProvider, includeInPayload })
const options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = { const options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = {
model: modelName, model: modelName,
messages: messages, messages: messages as any,
stream: true, stream: true,
...nativeToolsObj,
// max_completion_tokens: maxTokens, // max_completion_tokens: maxTokens,
} }
@ -168,9 +213,9 @@ const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage
onFinalMessage = newOnFinalMessage onFinalMessage = newOnFinalMessage
} }
// manually parse out tool results // manually parse out tool results if XML
if (chatMode) { if (!specialToolFormat) {
const { newOnText, newOnFinalMessage } = extractToolsWrapper(onText, onFinalMessage, chatMode) const { newOnText, newOnFinalMessage } = extractXMLToolsWrapper(onText, onFinalMessage, chatMode)
onText = newOnText onText = newOnText
onFinalMessage = newOnFinalMessage onFinalMessage = newOnFinalMessage
} }
@ -178,6 +223,10 @@ const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage
let fullReasoningSoFar = '' let fullReasoningSoFar = ''
let fullTextSoFar = '' let fullTextSoFar = ''
let toolName = ''
let toolId = ''
let toolParamsStr = ''
openai.chat.completions openai.chat.completions
.create(options) .create(options)
.then(async response => { .then(async response => {
@ -188,6 +237,17 @@ const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage
const newText = chunk.choices[0]?.delta?.content ?? '' const newText = chunk.choices[0]?.delta?.content ?? ''
fullTextSoFar += newText 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 // reasoning
let newReasoning = '' let newReasoning = ''
if (nameOfReasoningFieldInDelta) { if (nameOfReasoningFieldInDelta) {
@ -199,11 +259,13 @@ const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage
onText({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar }) onText({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar })
} }
// on final // on final
if (!fullTextSoFar && !fullReasoningSoFar) { if (!fullTextSoFar && !fullReasoningSoFar && !toolName) {
onError({ message: 'Void: Response from model was empty.', fullError: null }) onError({ message: 'Void: Response from model was empty.', fullError: null })
} }
else { else {
onFinalMessage({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar, anthropicReasoning: null }); 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) // when error/fail - this catches errors of both .create() and .then(for await)
@ -251,14 +313,48 @@ 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 ------------ // ------------ 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 { const {
modelName, modelName,
supportsSystemMessage, specialToolFormat,
contextWindow,
maxOutputTokens,
reasoningCapabilities,
} = getModelCapabilities(providerName, modelName_) } = getModelCapabilities(providerName, modelName_)
const thisConfig = settingsOfProvider.anthropic const thisConfig = settingsOfProvider.anthropic
@ -269,26 +365,32 @@ const sendAnthropicChat = ({ messages: messages_, providerName, onText, onFinalM
const includeInPayload = providerReasoningIOSettings?.input?.includeInPayload?.(reasoningInfo) || {} const includeInPayload = providerReasoningIOSettings?.input?.includeInPayload?.(reasoningInfo) || {}
// anthropic-specific - max tokens // anthropic-specific - max tokens
const maxTokens = reasoningInfo?.isReasoningEnabled && reasoningCapabilities ? reasoningCapabilities.reasoningMaxOutputTokens : maxOutputTokens 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 // instance
const { messages, separateSystemMessageStr } = prepareMessages({ messages: messages_, aiInstructions, supportsSystemMessage, supportsAnthropicReasoningSignature: true, contextWindow, maxOutputTokens: maxTokens })
const anthropic = new Anthropic({ const anthropic = new Anthropic({
apiKey: thisConfig.apiKey, apiKey: thisConfig.apiKey,
dangerouslyAllowBrowser: true dangerouslyAllowBrowser: true
}); });
const stream = anthropic.messages.stream({ const stream = anthropic.messages.stream({
system: separateSystemMessageStr, system: separateSystemMessage ?? undefined,
messages: messages, messages: messages as AnthropicLLMChatMessage[],
model: modelName, model: modelName,
max_tokens: maxTokens ?? 4_096, // anthropic requires this max_tokens: maxTokens ?? 4_096, // anthropic requires this
...includeInPayload, ...includeInPayload,
...nativeToolsObj,
}) })
// manually parse out tool results // manually parse out tool results
if (chatMode) { if (!specialToolFormat) {
const { newOnText, newOnFinalMessage } = extractToolsWrapper(onText, onFinalMessage, chatMode) const { newOnText, newOnFinalMessage } = extractXMLToolsWrapper(onText, onFinalMessage, chatMode)
onText = newOnText onText = newOnText
onFinalMessage = newOnFinalMessage onFinalMessage = newOnFinalMessage
} }
@ -297,8 +399,8 @@ const sendAnthropicChat = ({ messages: messages_, providerName, onText, onFinalM
let fullText = '' let fullText = ''
let fullReasoning = '' let fullReasoning = ''
// let fullToolName = '' let fullToolName = ''
// let fullToolParams = '' let fullToolParams = ''
// there are no events for tool_use, it comes in at the end // there are no events for tool_use, it comes in at the end
stream.on('streamEvent', e => { stream.on('streamEvent', e => {
@ -320,10 +422,10 @@ const sendAnthropicChat = ({ messages: messages_, providerName, onText, onFinalM
fullReasoning += '[redacted_thinking]' fullReasoning += '[redacted_thinking]'
onText({ fullText, fullReasoning, }) onText({ fullText, fullReasoning, })
} }
// else if (e.content_block.type === 'tool_use') { else if (e.content_block.type === 'tool_use') {
// fullToolName += e.content_block.name ?? '' // anthropic gives us the tool name in the start block fullToolName += e.content_block.name ?? '' // anthropic gives us the tool name in the start block
// onText({ fullText, fullReasoning, }) onText({ fullText, fullReasoning, })
// } }
} }
// delta // delta
@ -336,17 +438,20 @@ const sendAnthropicChat = ({ messages: messages_, providerName, onText, onFinalM
fullReasoning += e.delta.thinking fullReasoning += e.delta.thinking
onText({ fullText, fullReasoning, }) onText({ fullText, fullReasoning, })
} }
// else if (e.delta.type === 'input_json_delta') { // tool use 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 fullToolParams += e.delta.partial_json ?? '' // anthropic gives us the partial delta (string) here - https://docs.anthropic.com/en/api/messages-streaming
// onText({ fullText, fullReasoning, }) onText({ fullText, fullReasoning, })
// } }
} }
}) })
// on done - (or when error/fail) - this is called AFTER last streamEvent // on done - (or when error/fail) - this is called AFTER last streamEvent
stream.on('finalMessage', (response) => { stream.on('finalMessage', (response) => {
const anthropicReasoning = response.content.filter(c => c.type === 'thinking' || c.type === 'redacted_thinking') 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])
const toolCallObj = toolCall ? { toolCall } : {}
onFinalMessage({ fullText, fullReasoning, anthropicReasoning, ...toolCallObj })
}) })
// on error // on error
stream.on('error', (error) => { stream.on('error', (error) => {
@ -360,7 +465,7 @@ const sendAnthropicChat = ({ messages: messages_, providerName, onText, onFinalM
// ------------ MISTRAL ------------ // ------------ MISTRAL ------------
// https://docs.mistral.ai/api/#tag/fim // 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_) const { modelName, supportsFIM } = getModelCapabilities(providerName, modelName_)
if (!supportsFIM) { if (!supportsFIM) {
if (modelName === modelName_) if (modelName === modelName_)
@ -369,7 +474,6 @@ const sendMistralFIM = ({ messages: messages_, onFinalMessage, onError, settings
onError({ message: `Model ${modelName_} (${modelName}) does not support FIM.`, fullError: null }) onError({ message: `Model ${modelName_} (${modelName}) does not support FIM.`, fullError: null })
return return
} }
const messages = prepareFIMMessage({ messages: messages_, aiInstructions })
const mistral = new MistralCore({ apiKey: settingsOfProvider.mistral.apiKey }) const mistral = new MistralCore({ apiKey: settingsOfProvider.mistral.apiKey })
fimComplete(mistral, fimComplete(mistral,
@ -378,7 +482,7 @@ const sendMistralFIM = ({ messages: messages_, onFinalMessage, onError, settings
prompt: messages.prefix, prompt: messages.prefix,
suffix: messages.suffix, suffix: messages.suffix,
stream: false, stream: false,
maxTokens: messages.maxTokens, maxTokens: 300,
stop: messages.stopTokens, stop: messages.stopTokens,
}) })
.then(async response => { .then(async response => {
@ -426,12 +530,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 thisConfig = settingsOfProvider.ollama
const ollama = newOllamaSDK({ endpoint: thisConfig.endpoint }) const ollama = newOllamaSDK({ endpoint: thisConfig.endpoint })
const messages = prepareFIMMessage({ messages: messages_, aiInstructions, })
let fullText = '' let fullText = ''
ollama.generate({ ollama.generate({
model: modelName, model: modelName,
@ -439,7 +541,7 @@ const sendOllamaFIM = ({ messages: messages_, onFinalMessage, onError, settingsO
suffix: messages.suffix, suffix: messages.suffix,
options: { options: {
stop: messages.stopTokens, stop: messages.stopTokens,
num_predict: messages.maxTokens, // max tokens num_predict: 300, // max tokens
// repeat_penalty: 1, // repeat_penalty: 1,
}, },
raw: true, raw: true,

View file

@ -11,7 +11,6 @@ import { sendLLMMessageToProviderImplementation } from './sendLLMMessage.impl.js
export const sendLLMMessage = ({ export const sendLLMMessage = ({
messagesType, messagesType,
aiInstructions,
messages: messages_, messages: messages_,
onText: onText_, onText: onText_,
onFinalMessage: onFinalMessage_, onFinalMessage: onFinalMessage_,
@ -22,6 +21,7 @@ export const sendLLMMessage = ({
modelSelection, modelSelection,
modelSelectionOptions, modelSelectionOptions,
chatMode, chatMode,
separateSystemMessage,
}: SendLLMMessageParams, }: SendLLMMessageParams,
metricsService: IMetricsService metricsService: IMetricsService
@ -108,12 +108,12 @@ export const sendLLMMessage = ({
} }
const { sendFIM, sendChat } = implementation const { sendFIM, sendChat } = implementation
if (messagesType === 'chatMessages') { 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 return
} }
if (messagesType === 'FIMMessage') { if (messagesType === 'FIMMessage') {
if (sendFIM) { 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 return
} }
onError({ message: `Error: This provider does not support Autocomplete yet.`, fullError: null }) onError({ message: `Error: This provider does not support Autocomplete yet.`, fullError: null })