Compare commits

...

34 Commits

Author SHA1 Message Date
Andrew Pareles a0fa104d23 Merge pull request #401 from voideditor/model-selection
Model selection
2025-04-15 02:34:33 -07:00
Andrew Pareles 7fd0b133e5 misc prompt improvements 2025-04-15 02:30:37 -07:00
Andrew Pareles 321876932a loading 2025-04-15 01:51:59 -07:00
Andrew Pareles eda8517c78 Merge pull request #400 from voideditor/model-selection
GPT 4.1 and Claude 3.7 Thinking with Tools
2025-04-14 23:52:58 -07:00
Andrew Pareles 6ce37f7627 misc 2025-04-14 23:47:14 -07:00
Andrew Pareles 18b76885f5 1.2.1 2025-04-14 23:17:33 -07:00
Andrew Pareles ba8644fbb6 read_file improvements 2025-04-14 23:12:10 -07:00
Andrew Pareles ee959ded08 4.1 and 3.7 work! 2025-04-14 22:57:29 -07:00
Mathew Pareles 499a47904e style 2025-04-14 22:53:39 -07:00
Mathew Pareles 5ac03bb917 fix 2025-04-14 22:47:10 -07:00
Mathew Pareles 922f3ba2d7 fix current file 2025-04-14 22:46:54 -07:00
Mathew Pareles a189629341 fix current file 2025-04-14 22:46:15 -07:00
Andrew Pareles d2098f71eb support 4.1 native tools and 3.7 native thinking... 2025-04-14 22:01:46 -07:00
Mathew Pareles 77c59f2e80 lint error severity must be warning or error 2025-04-14 14:41:48 -07:00
Mathew Pareles e6ecad3f65 desc 2025-04-14 14:34:04 -07:00
Mathew Pareles ee43ec99ea duplicate tooltips 2025-04-14 13:59:32 -07:00
Andrew Pareles e77039e9ad Merge pull request #399 from voideditor/model-selection
1.2.0
2025-04-14 13:38:12 -07:00
Andrew Pareles 6aa1375067 1.2.0 2025-04-14 13:36:43 -07:00
Andrew Pareles fe017948c6 Merge pull request #398 from voideditor/model-selection
GPT 4.1 + fixes
2025-04-14 13:34:33 -07:00
Andrew Pareles 9b3d4cbdba lint errors 2025-04-14 13:33:49 -07:00
Andrew Pareles a8cf0b361f prompt 2025-04-14 13:29:13 -07:00
Andrew Pareles 680125baec hot-swap out default models on mount 2025-04-14 12:48:44 -07:00
Andrew Pareles 0db4f7118d Merge remote-tracking branch 'origin/main' into model-selection 2025-04-14 12:31:49 -07:00
Andrew Pareles 2ea20243a8 lint error UI + misc 2025-04-14 12:31:31 -07:00
Andrew Pareles 4d02381dab Merge pull request #321 from jcommaret/feat_mistral-199
[Provider]Mistral -😻- Mistral working : [OpenAICompatibleModel Chat + tools +  sendMistralFIM]
2025-04-14 12:03:53 -07:00
Andrew Pareles a80883c10f 4.1 fallback for OSS providers 2025-04-14 11:10:52 -07:00
Andrew Pareles 20b0fd6af9 add 4.1 2025-04-14 10:57:26 -07:00
Andrew Pareles 5171f7244f Merge pull request #397 from voideditor/model-selection
Misc
2025-04-14 01:28:56 -07:00
Andrew Pareles 43708d25e7 1.1.0 2025-04-14 01:28:37 -07:00
Andrew Pareles 2b54733242 disable onboarding 2025-04-13 22:07:30 -07:00
Andrew Pareles cd77d1a1c4 Update README.md 2025-04-13 20:38:46 -07:00
Andrew Pareles 37af09add8 message 2025-04-13 12:04:17 -07:00
Andrew Pareles ade1e70536 Merge pull request #395 from voideditor/model-selection
minor fixes
2025-04-12 20:16:39 -07:00
Andrew Pareles 159e0b677f minor fixes 2025-04-12 20:16:20 -07:00
31 changed files with 1298 additions and 1014 deletions
+1 -1
View File
@@ -27,7 +27,7 @@ This repo contains the full sourcecode for Void. We are currently in [open beta]
1. Feel free to attend a weekly meeting in our Discord channel if you'd like to contribute!
2. To get started working on Void, see [Contributing](https://github.com/voideditor/void/blob/main/HOW_TO_CONTRIBUTE.md).
2. To get started working on Void, see [here](https://github.com/voideditor/void/blob/main/HOW_TO_CONTRIBUTE.md).
3. We're open to collaborations and suggestions of all types - just reach out.
+1 -1
View File
@@ -1,7 +1,7 @@
{
"nameShort": "Void",
"nameLong": "Void",
"voidVersion": "1.0.3",
"voidVersion": "1.2.1",
"applicationName": "void",
"dataFolderName": ".void-editor",
"win32MutexName": "voideditor",
@@ -34,7 +34,7 @@
// // const result = await new Promise((res, rej) => {
// // sendLLMMessage({
// // messages,
// // tools: ['search_files'],
// // tools: ['search_for_files'],
// // onFinalMessage: ({ result: r, }) => {
// // res(r)
// // },
@@ -73,7 +73,7 @@
// // const result = new Promise((res, rej) => {
// // sendLLMMessage({
// // messages,
// // tools: ['search_files'],
// // tools: ['search_for_files'],
// // onResult: (r) => {
// // res(r)
// // }
@@ -20,6 +20,7 @@ import { ILLMMessageService } from '../common/sendLLMMessageService.js';
import { isWindows } from '../../../../base/common/platform.js';
import { IVoidSettingsService } from '../common/voidSettingsService.js';
import { FeatureName } from '../common/voidSettingsTypes.js';
import { IConvertToLLMMessageService } from './convertToLLMMessageService.js';
// import { IContextGatheringService } from './contextGatheringService.js';
@@ -791,18 +792,21 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
const featureName: FeatureName = 'Autocomplete'
const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName]
const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[featureName][modelSelection.providerName]?.[modelSelection.modelName] : undefined
const aiInstructions = this._settingsService.state.globalSettings.aiInstructions
// set parameters of `newAutocompletion` appropriately
newAutocompletion.llmPromise = new Promise((resolve, reject) => {
const requestId = this._llmMessageService.sendLLMMessage({
messagesType: 'FIMMessage',
messages: {
prefix: llmPrefix,
suffix: llmSuffix,
stopTokens: stopTokens,
},
messages: this._convertToLLMMessageService.prepareFIMMessage({
messages: {
prefix: llmPrefix,
suffix: llmSuffix,
stopTokens: stopTokens,
},
aiInstructions
}),
modelSelection,
modelSelectionOptions,
logging: { loggingName: 'Autocomplete' },
@@ -890,6 +894,7 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
@IEditorService private readonly _editorService: IEditorService,
@IModelService private readonly _modelService: IModelService,
@IVoidSettingsService private readonly _settingsService: IVoidSettingsService,
@IConvertToLLMMessageService private readonly _convertToLLMMessageService: IConvertToLLMMessageService
// @IContextGatheringService private readonly _contextGatheringService: IContextGatheringService,
) {
super()
@@ -11,11 +11,10 @@ import { IStorageService, StorageScope, StorageTarget } from '../../../../platfo
import { URI } from '../../../../base/common/uri.js';
import { Emitter, Event } from '../../../../base/common/event.js';
import { ILLMMessageService } from '../common/sendLLMMessageService.js';
import { chat_userMessageContent, chat_systemMessage, ToolName, toolCallXMLStr, } from '../common/prompt/prompts.js';
import { getErrorMessage, LLMChatMessage, RawToolCallObj, RawToolParamsObj } from '../common/sendLLMMessageTypes.js';
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
import { chat_userMessageContent, ToolName, } from '../common/prompt/prompts.js';
import { getErrorMessage, RawToolCallObj, RawToolParamsObj } from '../common/sendLLMMessageTypes.js';
import { generateUuid } from '../../../../base/common/uuid.js';
import { ChatMode, FeatureName, ModelSelection, ModelSelectionOptions } from '../common/voidSettingsTypes.js';
import { FeatureName, ModelSelection, ModelSelectionOptions } from '../common/voidSettingsTypes.js';
import { IVoidSettingsService } from '../common/voidSettingsService.js';
import { ToolCallParams, ToolResultType, toolNamesThatRequireApproval } from '../common/toolsServiceTypes.js';
import { IToolsService } from './toolsService.js';
@@ -23,7 +22,6 @@ import { CancellationToken } from '../../../../base/common/cancellation.js';
import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js';
import { ChatMessage, CheckpointEntry, CodespanLocationLink, StagingSelectionItem, ToolMessage } from '../common/chatThreadServiceTypes.js';
import { Position } from '../../../../editor/common/core/position.js';
import { ITerminalToolService } from './terminalToolService.js';
import { IMetricsService } from '../common/metricsService.js';
import { shorten } from '../../../../base/common/labels.js';
import { IVoidModelService } from '../common/voidModelService.js';
@@ -33,11 +31,35 @@ import { findLast, findLastIdx } from '../../../../base/common/arraysFind.js';
import { IEditCodeService } from './editCodeServiceInterface.js';
import { VoidFileSnapshot } from '../common/editCodeServiceTypes.js';
import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js';
import { IModelService } from '../../../../editor/common/services/model.js';
import { IDirectoryStrService } from './directoryStrService.js';
import { truncate } from '../../../../base/common/strings.js';
import { THREAD_STORAGE_KEY } from '../common/storageKeys.js';
import { deepClone } from '../../../../base/common/objects.js';
import { IConvertToLLMMessageService } from './convertToLLMMessageService.js';
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,
@ILLMMessageService private readonly _llmMessageService: ILLMMessageService,
@IToolsService private readonly _toolsService: IToolsService,
@IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService,
@IVoidSettingsService private readonly _settingsService: IVoidSettingsService,
@ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService,
@ITerminalToolService private readonly _terminalToolService: ITerminalToolService,
@IMetricsService private readonly _metricsService: IMetricsService,
@IEditorService private readonly _editorService: IEditorService,
@ICodeEditorService private readonly _codeEditorService: ICodeEditorService,
@IEditCodeService private readonly _editCodeService: IEditCodeService,
@INotificationService private readonly _notificationService: INotificationService,
@IModelService private readonly _modelService: IModelService,
@IDirectoryStrService private readonly _directoryStrService: IDirectoryStrService,
@IConvertToLLMMessageService private readonly _convertToLLMMessagesService: IConvertToLLMMessageService,
) {
super()
this.state = { allThreads: {}, currentThreadId: null as unknown as string } // default state
@@ -288,14 +307,16 @@ class ChatThreadService extends Disposable implements IChatThreadService {
}
const oldStagingSelections = this.getCurrentThreadState().stagingSelections || [];
const fileIsAlreadyHere = oldStagingSelections.some(s => s.type === 'File' && s.uri.fsPath === newStagingSelection.uri.fsPath)
if (fileIsAlreadyHere) return
// remove all old selectons that are marked as `wasAddedAsCurrentFile`, and add new selection
const newStagingSelections: StagingSelectionItem[] = [
...oldStagingSelections.filter(s => !s.state?.wasAddedAsCurrentFile),
newStagingSelection
]
// remove all old selectons that are marked as `wasAddedAsCurrentFile`
const newStagingSelections: StagingSelectionItem[] = oldStagingSelections.filter(s => s.state && !s.state.wasAddedAsCurrentFile)
const fileIsAlreadyHere = oldStagingSelections.some(s => s.type === 'File' && s.uri.fsPath === newStagingSelection.uri.fsPath)
if (!fileIsAlreadyHere) {
newStagingSelections.push(newStagingSelection)
}
this.setCurrentThreadState({ stagingSelections: newStagingSelections });
}
@@ -454,10 +475,10 @@ class ChatThreadService extends Disposable implements IChatThreadService {
}
else return
const { name } = lastMsg
const { name, id, rawParams } = lastMsg
const errorMessage = this.errMsgs.rejected
this._updateLatestTool(threadId, { role: 'tool', type: 'rejected', params: params, name: name, content: errorMessage, result: null })
this._updateLatestTool(threadId, { role: 'tool', type: 'rejected', params: params, name: name, content: errorMessage, result: null, id, rawParams })
this._setStreamState(threadId, {}, 'set')
}
@@ -482,7 +503,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
const llmCancelToken = this.streamState[threadId]?.streamingToken
if (llmCancelToken !== undefined) { this._llmMessageService.abort(llmCancelToken) }
this._addMessageToThread(threadId, { role: 'assistant', displayContent: displayContentSoFar, reasoning: reasoningSoFar, toolCall: toolCallSoFar, anthropicReasoning: null })
this._addMessageToThread(threadId, { role: 'assistant', displayContent: displayContentSoFar, reasoning: reasoningSoFar, anthropicReasoning: null })
if (toolCallSoFar) {
this._addMessageToThread(threadId, { role: 'interrupted_streaming_tool', name: toolCallSoFar.name })
@@ -505,69 +526,12 @@ class ChatThreadService extends Disposable implements IChatThreadService {
private readonly _currentlyRunningToolInterruptor: { [threadId: string]: (() => void) | undefined } = {}
// system message
private _generateSystemMessage = async (chatMode: ChatMode) => {
const workspaceFolders = this._workspaceContextService.getWorkspace().folders.map(f => f.uri.fsPath)
const openedURIs = this._modelService.getModels().filter(m => m.isAttachedToEditor()).map(m => m.uri.fsPath) || [];
const activeURI = this._editorService.activeEditor?.resource?.fsPath;
const directoryStr = await this._directoryStrService.getAllDirectoriesStr({
cutOffMessage: chatMode === 'agent' || chatMode === 'gather' ? `...Directories string cut off, use tools to read more...`
: `...Directories string cut off, ask user for more if necessary...`
})
const runningTerminalIds = this._terminalToolService.listTerminalIds()
const systemMessage = chat_systemMessage({ workspaceFolders, openedURIs, directoryStr, activeURI, runningTerminalIds, chatMode })
return systemMessage
}
private _generateLLMMessages = async (threadId: string) => {
const thread = this.state.allThreads[threadId]
if (!thread) return []
const chatMessages = deepClone(thread.messages)
const llmChatMessages: LLMChatMessage[] = []
// merge tools into user message
for (const c of chatMessages) {
if (c.role === 'assistant') {
// if called a tool, re-add its XML to the message
// alternatively, could just hold onto the original output, but this way requires less piping raw strings everywhere
let content = c.displayContent
if (c.toolCall) {
content = `${content}\n\n${toolCallXMLStr(c.toolCall)}`
}
llmChatMessages.push({ role: c.role, content: content, anthropicReasoning: c.anthropicReasoning })
}
else if (c.role === 'user' || c.role === 'tool') {
if (c.role === 'tool')
c.content = `<${c.name}_result>\n${c.content}\n</${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
private _runToolCall = async (
threadId: string,
toolName: ToolName,
opts: { preapproved: true, validatedParams: ToolCallParams[ToolName] } | { preapproved: false, unvalidatedToolParams: RawToolParamsObj },
toolId: string,
opts: { preapproved: true, unvalidatedToolParams: RawToolParamsObj, validatedParams: ToolCallParams[ToolName] } | { preapproved: false, unvalidatedToolParams: RawToolParamsObj },
): Promise<{ awaitingUserApproval?: boolean, interrupted?: boolean }> => {
// compute these below
@@ -582,7 +546,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
toolParams = params
} catch (error) {
const errorMessage = getErrorMessage(error)
this._addMessageToThread(threadId, { role: 'tool', type: 'invalid_params', params: null, result: null, name: toolName, content: errorMessage, })
this._addMessageToThread(threadId, { role: 'tool', type: 'invalid_params', rawParams: opts.unvalidatedToolParams, result: null, name: toolName, content: errorMessage, id: toolId, })
return {}
}
// once validated, add checkpoint for edit
@@ -593,7 +557,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
if (toolRequiresApproval) {
const autoApprove = this._settingsService.state.globalSettings.autoApprove
// add a tool_request because we use it for UI if a tool is loading (this should be improved in the future)
this._addMessageToThread(threadId, { role: 'tool', type: 'tool_request', content: '(never)', result: null, name: toolName, params: toolParams })
this._addMessageToThread(threadId, { role: 'tool', type: 'tool_request', content: '(never)', result: null, name: toolName, params: toolParams, id: toolId, rawParams: opts.unvalidatedToolParams })
if (!autoApprove) {
return { awaitingUserApproval: true }
}
@@ -603,9 +567,11 @@ class ChatThreadService extends Disposable implements IChatThreadService {
toolParams = opts.validatedParams
}
// 3. call the tool
this._setStreamState(threadId, { isRunning: 'tool' }, 'merge')
this._updateLatestTool(threadId, { role: 'tool', type: 'running_now', name: toolName, params: toolParams, content: '(value not received yet...)', result: null })
this._updateLatestTool(threadId, { role: 'tool', type: 'running_now', name: toolName, params: toolParams, content: '(value not received yet...)', result: null, id: toolId, rawParams: opts.unvalidatedToolParams })
let interrupted = false
try {
@@ -624,7 +590,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
const errorMessage = getErrorMessage(error)
this._updateLatestTool(threadId, { role: 'tool', type: 'tool_error', params: toolParams, result: errorMessage, name: toolName, content: errorMessage, })
this._updateLatestTool(threadId, { role: 'tool', type: 'tool_error', params: toolParams, result: errorMessage, name: toolName, content: errorMessage, id: toolId, rawParams: opts.unvalidatedToolParams })
return {}
}
@@ -633,12 +599,12 @@ class ChatThreadService extends Disposable implements IChatThreadService {
toolResultStr = this._toolsService.stringOfResult[toolName](toolParams as any, toolResult as any)
} catch (error) {
const errorMessage = this.errMsgs.errWhenStringifying(error)
this._updateLatestTool(threadId, { role: 'tool', type: 'tool_error', params: toolParams, result: errorMessage, name: toolName, content: errorMessage, })
this._updateLatestTool(threadId, { role: 'tool', type: 'tool_error', params: toolParams, result: errorMessage, name: toolName, content: errorMessage, id: toolId, rawParams: opts.unvalidatedToolParams })
return {}
}
// 5. add to history and keep going
this._updateLatestTool(threadId, { role: 'tool', type: 'success', params: toolParams, result: toolResult, name: toolName, content: toolResultStr, })
this._updateLatestTool(threadId, { role: 'tool', type: 'success', params: toolParams, result: toolResult, name: toolName, content: toolResultStr, id: toolId, rawParams: opts.unvalidatedToolParams })
return {}
};
@@ -659,6 +625,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
callThisToolFirst?: ToolMessage<ToolName> & { type: 'tool_request' }
}) {
// above just defines helpers, below starts the actual function
const { chatMode } = this._settingsService.state.globalSettings // should not change as we loop even if user changes it, so it goes here
@@ -672,7 +639,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
// before enter loop, call tool
if (callThisToolFirst) {
const { interrupted } = await this._runToolCall(threadId, callThisToolFirst.name, { preapproved: true, validatedParams: callThisToolFirst.params })
const { interrupted } = await this._runToolCall(threadId, callThisToolFirst.name, callThisToolFirst.id, { preapproved: true, unvalidatedToolParams: callThisToolFirst.rawParams, validatedParams: callThisToolFirst.params })
if (interrupted) return
}
@@ -688,34 +655,36 @@ class ChatThreadService extends Disposable implements IChatThreadService {
// send llm message
this._setStreamState(threadId, { isRunning: 'LLM' }, 'merge')
const systemMessage = await this._generateSystemMessage(chatMode)
const llmMessages = await this._generateLLMMessages(threadId)
const messages: LLMChatMessage[] = [
{ role: 'system', content: systemMessage },
...llmMessages
]
const chatMessages = this.state.allThreads[threadId]?.messages ?? []
const { messages, separateSystemMessage } = await this._convertToLLMMessagesService.prepareLLMChatMessages({
chatMessages,
modelSelection,
chatMode
})
const llmCancelToken = this._llmMessageService.sendLLMMessage({
messagesType: 'chatMessages',
chatMode,
messages,
messages: messages,
modelSelection,
modelSelectionOptions,
logging: { loggingName: `Chat - ${chatMode}`, loggingExtras: { threadId, nMessagesSent, chatMode } },
separateSystemMessage: separateSystemMessage,
onText: ({ fullText, fullReasoning, toolCall }) => {
this._setStreamState(threadId, { displayContentSoFar: fullText, reasoningSoFar: fullReasoning, toolCallSoFar: toolCall }, 'merge')
},
onFinalMessage: async ({ fullText, fullReasoning, toolCall, anthropicReasoning, }) => {
this._addMessageToThread(threadId, { role: 'assistant', displayContent: fullText, reasoning: fullReasoning, toolCall, anthropicReasoning })
this._addMessageToThread(threadId, { role: 'assistant', displayContent: fullText, reasoning: fullReasoning, anthropicReasoning })
this._setStreamState(threadId, { displayContentSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined, toolCallSoFar: undefined }, 'merge')
resMessageIsDonePromise(toolCall) // resolve with tool calls
},
onError: (error) => {
const messageSoFar = this.streamState[threadId]?.displayContentSoFar ?? ''
const reasoningSoFar = this.streamState[threadId]?.reasoningSoFar ?? ''
const toolCallSoFar = this.streamState[threadId]?.toolCallSoFar
// const toolCallSoFar = this.streamState[threadId]?.toolCallSoFar
// add assistant's message to chat history, and clear selection
this._addMessageToThread(threadId, { role: 'assistant', displayContent: messageSoFar, reasoning: reasoningSoFar, toolCall: toolCallSoFar, anthropicReasoning: null })
this._addMessageToThread(threadId, { role: 'assistant', displayContent: messageSoFar, reasoning: reasoningSoFar, anthropicReasoning: null })
this._setStreamState(threadId, { error }, 'set')
resMessageIsDonePromise()
},
@@ -742,7 +711,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
// call tool if there is one
const tool: RawToolCallObj | undefined = toolCall
if (tool) {
const { awaitingUserApproval, interrupted } = await this._runToolCall(threadId, tool.name, { preapproved: false, unvalidatedToolParams: tool.rawParams })
const { awaitingUserApproval, interrupted } = await this._runToolCall(threadId, tool.name, tool.id, { preapproved: false, unvalidatedToolParams: tool.rawParams })
// stop if interrupted. we don't have to do this for llmMessage because we have a stream token for it and onAbort gets called, but we don't have the equivalent for tools.
// just detect tool interruption which is the same as chat interruption right now
@@ -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"
}
}
}
*/
@@ -417,7 +417,6 @@ class DirectoryStrService extends Disposable implements IDirectoryStrService {
break;
}
}
console.log('cutoff!!!!!!!', str, cutOffMessage)
if (cutOff) {
return `${str}\n${cutOffMessage}`
@@ -47,6 +47,7 @@ import { IVoidModelService } from '../common/voidModelService.js';
import { deepClone } from '../../../../base/common/objects.js';
import { acceptBg, acceptBorder, buttonFontSize, buttonTextColor, rejectBg, rejectBorder } from '../common/helpers/colors.js';
import { DiffArea, Diff, CtrlKZone, VoidFileSnapshot, DiffAreaSnapshotEntry, diffAreaSnapshotKeys, DiffZone, TrackingZone, ComputedDiff } from '../common/editCodeServiceTypes.js';
import { IConvertToLLMMessageService } from './convertToLLMMessageService.js';
const configOfBG = (color: Color) => {
return { dark: color, light: color, hcDark: color, hcLight: color, }
@@ -202,6 +203,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
@IVoidSettingsService private readonly _settingsService: IVoidSettingsService,
// @IFileService private readonly _fileService: IFileService,
@IVoidModelService private readonly _voidModelService: IVoidModelService,
@IConvertToLLMMessageService private readonly _convertToLLMMessageService: IConvertToLLMMessageService,
) {
super();
@@ -1233,7 +1235,6 @@ class EditCodeService extends Disposable implements IEditCodeService {
_removeStylesFns: new Set(),
}
console.log('FIRING START STREAMING IN DIFFZONE!!!')
const diffZone = this._addDiffArea(adding)
this._onDidChangeStreamingInDiffZone.fire({ uri, diffareaid: diffZone.diffareaid })
this._onDidAddOrDeleteDiffZones.fire({ uri })
@@ -1267,6 +1268,10 @@ class EditCodeService extends Disposable implements IEditCodeService {
private _initializeWriteoverStream(opts: StartApplyingOpts): [DiffZone, Promise<void>] | undefined {
const { from, } = opts
const featureName: FeatureName = opts.from === 'ClickApply' ? 'Apply' : 'Ctrl+K'
const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName]
const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[featureName][modelSelection.providerName]?.[modelSelection.modelName] : undefined
const uri = this._getURIBeforeStartApplying(opts)
if (!uri) return
@@ -1300,12 +1305,16 @@ class EditCodeService extends Disposable implements IEditCodeService {
const originalCode = startRange === 'fullFile' ? originalFileCode : originalFileCode.split('\n').slice((startRange[0] - 1), (startRange[1] - 1) + 1).join('\n')
const language = model.getLanguageId()
let messages: LLMChatMessage[]
let separateSystemMessage: string | undefined
if (from === 'ClickApply') {
const userContent = rewriteCode_userMessage({ originalCode, applyStr: opts.applyStr, language })
messages = [
{ role: 'system', content: rewriteCode_systemMessage, },
{ role: 'user', content: userContent, }
]
const { messages: a, separateSystemMessage: b } = this._convertToLLMMessageService.prepareLLMSimpleMessages({
systemMessage: rewriteCode_systemMessage,
simpleMessages: [{ role: 'user', content: rewriteCode_userMessage({ originalCode, applyStr: opts.applyStr, language }), }],
featureName,
modelSelection,
})
messages = a
separateSystemMessage = b
}
else if (from === 'QuickEdit') {
if (!ctrlKZoneIfQuickEdit) return
@@ -1316,11 +1325,16 @@ class EditCodeService extends Disposable implements IEditCodeService {
const endLine = startRange === 'fullFile' ? model.getLineCount() : startRange[1]
const { prefix, suffix } = voidPrefixAndSuffix({ fullFileStr: originalFileCode, startLine, endLine })
const userContent = ctrlKStream_userMessage({ selection: originalCode, instructions: instructions, prefix, suffix, fimTags: quickEditFIMTags, language })
// type: 'messages',
messages = [
{ role: 'system', content: ctrlKStream_systemMessage({ quickEditFIMTags: quickEditFIMTags }), },
{ role: 'user', content: userContent, }
]
const { messages: a, separateSystemMessage: b } = this._convertToLLMMessageService.prepareLLMSimpleMessages({
systemMessage: ctrlKStream_systemMessage({ quickEditFIMTags: quickEditFIMTags }),
simpleMessages: [{ role: 'user', content: userContent, }],
featureName,
modelSelection,
})
messages = a
separateSystemMessage = b
}
else { throw new Error(`featureName ${from} is invalid`) }
@@ -1384,10 +1398,6 @@ class EditCodeService extends Disposable implements IEditCodeService {
const latestStreamLocationMutable: StreamLocationMutable = { line: diffZone.startLine, addedSplitYet: false, col: 1, originalCodeStartLine: 1 }
const featureName: FeatureName = opts.from === 'ClickApply' ? 'Apply' : 'Ctrl+K'
const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName]
const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[featureName][modelSelection.providerName]?.[modelSelection.modelName] : undefined
// allowed to throw errors - this is called inside a promise that handles everything
const runWriteover = async () => {
let shouldSendAnotherMessage = true
@@ -1410,6 +1420,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
messages,
modelSelection,
modelSelectionOptions,
separateSystemMessage,
chatMode: null, // not chat
onText: (params) => {
const { fullText: fullText_ } = params
@@ -1485,6 +1496,9 @@ class EditCodeService extends Disposable implements IEditCodeService {
private _initializeSearchAndReplaceStream(opts: StartApplyingOpts & { from: 'ClickApply' }): [DiffZone, Promise<void>] | undefined {
const { from, applyStr, } = opts
const featureName: FeatureName = 'Apply'
const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName]
const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[featureName][modelSelection.providerName]?.[modelSelection.modelName] : undefined
const uri = this._getURIBeforeStartApplying(opts)
if (!uri) return
@@ -1498,10 +1512,13 @@ class EditCodeService extends Disposable implements IEditCodeService {
// build messages - ask LLM to generate search/replace block text
const originalFileCode = model.getValue(EndOfLinePreference.LF)
const userMessageContent = searchReplace_userMessage({ originalCode: originalFileCode, applyStr: applyStr })
const messages: LLMChatMessage[] = [
{ role: 'system', content: searchReplace_systemMessage },
{ role: 'user', content: userMessageContent },
]
const { messages, separateSystemMessage: separateSystemMessage } = this._convertToLLMMessageService.prepareLLMSimpleMessages({
systemMessage: searchReplace_systemMessage,
simpleMessages: [{ role: 'user', content: userMessageContent, }],
featureName,
modelSelection,
})
// if URI is already streaming, return (should never happen, caller is responsible for checking)
if (this._uriIsStreaming(uri)) return
@@ -1593,10 +1610,6 @@ class EditCodeService extends Disposable implements IEditCodeService {
const addedTrackingZoneOfBlockNum: TrackingZone<SearchReplaceDiffAreaMetadata>[] = []
diffZone._streamState.line = 1
const featureName: FeatureName = 'Apply'
const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName]
const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[featureName][modelSelection.providerName]?.[modelSelection.modelName] : undefined
const N_RETRIES = 2
// allowed to throw errors - this is called inside a promise that handles everything
@@ -1628,6 +1641,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
messages,
modelSelection,
modelSelectionOptions,
separateSystemMessage,
chatMode: null, // not chat
onText: (params) => {
const { fullText } = params
@@ -1682,7 +1696,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
console.log('---------')
const content = errContentOfInvalidStr(originalBounds, block.orig, blockNum, blocks)
messages.push(
{ role: 'assistant', content: fullText, anthropicReasoning: null }, // latest output
{ role: 'assistant', content: fullText }, // latest output
{ role: 'user', content: content } // user explanation of what's wrong
)
@@ -162,7 +162,6 @@ export const useApplyButtonState = ({ applyBoxId, uri }: { applyBoxId: string, u
)
if (shouldUpdate) {
rerender(c => c + 1)
console.log('rerendering....')
}
}, [applyBoxId, uri]))
@@ -322,12 +321,12 @@ export const ApplyButtonsHTML = ({ codeStr, applyBoxId, reapplyIcon, uri }: { co
<IconShell1
Icon={X}
onClick={onReject}
{...tooltipPropsForApplyBlock({ tooltipName: 'Reject file' })}
{...tooltipPropsForApplyBlock({ tooltipName: 'Remove' })}
/>
<IconShell1
Icon={Check}
onClick={onAccept}
{...tooltipPropsForApplyBlock({ tooltipName: 'Accept file' })}
{...tooltipPropsForApplyBlock({ tooltipName: 'Keep' })}
/>
</>
}
@@ -57,7 +57,7 @@ const CodespanWithLink = ({ text, rawText, chatMessageLocation }: { text: string
const [didComputeCodespanLink, setDidComputeCodespanLink] = useState<boolean>(false)
let link = undefined
if (rawText.endsWith("`")) { // if codespan was completed
if (rawText.endsWith('`')) { // if codespan was completed
// get link from cache
link = chatThreadService.getCodespanLink({ codespanStr: text, messageIdx, threadId })
@@ -120,11 +120,11 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
return null;
}
if (t.type === "space") {
if (t.type === 'space') {
return <span>{t.raw}</span>
}
if (t.type === "code") {
if (t.type === 'code') {
const [firstLine, remainingContents] = separateOutFirstLine(t.text)
const firstLineIsURI = isValidUri(firstLine) && !codeURI
const contents = firstLineIsURI ? (remainingContents?.trimStart() || '') : t.text // exclude first-line URI from contents
@@ -152,7 +152,7 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
}
if (options.isApplyEnabled && chatMessageLocation) {
const isCodeblockClosed = t.raw.trimEnd().endsWith('```') // user should only be able to Apply when the code has been closed (t.raw ends with "```")
const isCodeblockClosed = t.raw.trimEnd().endsWith('```') // user should only be able to Apply when the code has been closed (t.raw ends with '```')
const applyBoxId = getApplyBoxId({
threadId: chatMessageLocation.threadId,
@@ -179,7 +179,7 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
/>
}
if (t.type === "heading") {
if (t.type === 'heading') {
const HeadingTag = `h${t.depth}` as keyof JSX.IntrinsicElements
@@ -188,7 +188,7 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
</HeadingTag>
}
if (t.type === "table") {
if (t.type === 'table') {
return (
<div>
<table>
@@ -217,14 +217,14 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
)
// return (
// <div>
// <table className={"min-w-full border border-void-bg-2"}>
// <table className={'min-w-full border border-void-bg-2'}>
// <thead>
// <tr className="bg-void-bg-1">
// <tr className='bg-void-bg-1'>
// {t.header.map((cell: any, index: number) => (
// <th
// key={index}
// className="px-4 py-2 border border-void-bg-2 font-semibold"
// style={{ textAlign: t.align[index] || "left" }}
// className='px-4 py-2 border border-void-bg-2 font-semibold'
// style={{ textAlign: t.align[index] || 'left' }}
// >
// {cell.raw}
// </th>
@@ -237,8 +237,8 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
// {row.map((cell: any, cellIndex: number) => (
// <td
// key={cellIndex}
// className={"px-4 py-2 border border-void-bg-2"}
// style={{ textAlign: t.align[cellIndex] || "left" }}
// className={'px-4 py-2 border border-void-bg-2'}
// style={{ textAlign: t.align[cellIndex] || 'left' }}
// >
// {cell.raw}
// </td>
@@ -251,32 +251,32 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
// )
}
if (t.type === "hr") {
if (t.type === 'hr') {
return <hr />
}
if (t.type === "blockquote") {
if (t.type === 'blockquote') {
return <blockquote>{t.text}</blockquote>
}
if (t.type === 'list_item') {
return <li>
<input type="checkbox" checked={t.checked} readOnly />
<input type='checkbox' checked={t.checked} readOnly />
<span>
<ChatMarkdownRender chatMessageLocation={chatMessageLocation} string={t.text} inPTag={true} codeURI={codeURI} {...options} />
</span>
</li>
}
if (t.type === "list") {
const ListTag = t.ordered ? "ol" : "ul"
if (t.type === 'list') {
const ListTag = t.ordered ? 'ol' : 'ul'
return (
<ListTag start={t.start ? t.start : undefined}>
{t.items.map((item, index) => (
<li key={index}>
{item.task && (
<input type="checkbox" checked={item.checked} readOnly />
<input type='checkbox' checked={item.checked} readOnly />
)}
<span>
<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 = <>
{t.tokens.map((token, index) => (
<RenderToken key={index}
@@ -304,34 +304,28 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
return <p>{contents}</p>
}
if (t.type === "html") {
const contents = t.raw
if (inPTag) return <span className='block'>{contents}</span>
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>
}
if (t.type === "def") {
if (t.type === 'def') {
return <></> // Definitions are typically not rendered
}
if (t.type === "link") {
if (t.type === 'link') {
return (
<a
onClick={() => { window.open(t.href) }}
href={t.href}
title={t.title ?? undefined}
className='underline cursor-pointer hover:brightness-90 transition-all duration-200'
className='underline cursor-pointer hover:brightness-90 transition-all duration-200 text-void-fg-2'
>
{t.text}
</a>
)
}
if (t.type === "image") {
if (t.type === 'image') {
return <img
src={t.href}
alt={t.text}
@@ -340,16 +334,16 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
/>
}
if (t.type === "strong") {
if (t.type === 'strong') {
return <strong>{t.text}</strong>
}
if (t.type === "em") {
if (t.type === 'em') {
return <em>{t.text}</em>
}
// inline code
if (t.type === "codespan") {
if (t.type === 'codespan') {
if (options.isLinkDetectionEnabled && chatMessageLocation) {
return <CodespanWithLink
@@ -363,18 +357,18 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
return <Codespan text={t.text} />
}
if (t.type === "br") {
if (t.type === 'br') {
return <br />
}
// strikethrough
if (t.type === "del") {
if (t.type === 'del') {
return <del>{t.text}</del>
}
// default
return (
<div className="bg-orange-50 rounded-sm overflow-hidden p-2">
<span className="text-sm text-orange-500">Unknown token rendered...</span>
<div className='bg-orange-50 rounded-sm overflow-hidden p-2'>
<span className='text-sm text-orange-500'>Unknown token rendered...</span>
</div>
)
}
@@ -20,16 +20,15 @@ import { VOID_OPEN_SETTINGS_ACTION_ID } from '../../../voidSettingsPane.js';
import { ChatMode, FeatureName, isFeatureNameDisabled } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js';
import { WarningBox } from '../void-settings-tsx/WarningBox.js';
import { getModelCapabilities, getIsReasoningEnabledState } from '../../../../common/modelCapabilities.js';
import { AlertTriangle, Ban, Check, ChevronRight, Dot, FileIcon, Pencil, Undo, Undo2, X } from 'lucide-react';
import { AlertTriangle, Ban, Check, ChevronRight, Dot, FileIcon, Pencil, Undo, Undo2, X, Flag } from 'lucide-react';
import { ChatMessage, CheckpointEntry, StagingSelectionItem, ToolMessage } from '../../../../common/chatThreadServiceTypes.js';
import { LintErrorItem, ToolCallParams, ToolNameWithApproval } from '../../../../common/toolsServiceTypes.js';
import { ApplyButtonsHTML, CopyButton, IconShell1, JumpToFileButton, JumpToTerminalButton, StatusIndicator, StatusIndicatorForApplyButton, useApplyButtonState } from '../markdown/ApplyBlockHoverButtons.js';
import { IsRunningType } from '../../../chatThreadService.js';
import { acceptAllBg, acceptBorder, buttonFontSize, buttonTextColor, rejectAllBg, rejectBg, rejectBorder } from '../../../../common/helpers/colors.js';
import { PlacesType } from 'react-tooltip';
import { ToolName, toolNames } from '../../../../common/prompt/prompts.js';
import { error } from 'console';
import { RawToolCallObj } from '../../../../common/sendLLMMessageTypes.js';
import { MAX_FILE_CHARS_PAGE } from '../../../toolsService.js';
@@ -662,7 +661,8 @@ type ToolHeaderParams = {
children?: React.ReactNode;
bottomChildren?: React.ReactNode;
onClick?: () => void;
isOpen?: boolean,
isOpen?: boolean;
className?: string;
}
const ToolHeaderWrapper = ({
@@ -679,7 +679,7 @@ const ToolHeaderWrapper = ({
isOpen,
isRejected,
className, // applies to the main content
}: ToolHeaderParams & { className?: string }) => {
}: ToolHeaderParams) => {
const [isOpen_, setIsOpen] = useState(false);
const isExpanded = isOpen !== undefined ? isOpen : isOpen_
@@ -1172,11 +1172,12 @@ const titleOfToolName = {
'ls_dir': { done: 'Inspected folder', proposed: 'Inspect folder', running: loadingTitleWrapper('Inspecting folder') },
'get_dir_structure': { done: 'Inspected folder', proposed: 'Inspect folder', running: loadingTitleWrapper('Inspecting folder') },
'search_pathnames_only': { done: 'Searched by file name', proposed: 'Search by file name', running: loadingTitleWrapper('Searching by file name') },
'search_files': { done: 'Searched', proposed: 'Search', running: loadingTitleWrapper('Searching') },
'search_for_files': { done: 'Searched', proposed: 'Search', running: loadingTitleWrapper('Searching') },
'create_file_or_folder': { done: `Created`, proposed: `Create`, running: loadingTitleWrapper(`Creating`) },
'delete_file_or_folder': { done: `Deleted`, proposed: `Delete`, running: loadingTitleWrapper(`Deleting`) },
'edit_file': { done: `Edited file`, proposed: 'Edit file', running: loadingTitleWrapper('Editing file') },
'run_terminal_command': { done: `Ran terminal`, proposed: 'Run terminal', running: loadingTitleWrapper('Running terminal') }
'run_terminal_command': { done: `Ran terminal`, proposed: 'Run terminal', running: loadingTitleWrapper('Running terminal') },
'read_lint_errors': { done: `Read lint errors`, proposed: 'Read lint errors', running: loadingTitleWrapper('Reading lint errors') },
} as const satisfies Record<ToolName, { done: any, proposed: any, running: any }>
const getTitle = (toolMessage: Pick<ChatMessage & { role: 'tool' }, 'name' | 'type'>): React.ReactNode => {
@@ -1205,8 +1206,8 @@ const toolNameToDesc = (toolName: ToolName, _toolParams: ToolCallParams[ToolName
} else if (toolName === 'search_pathnames_only') {
const toolParams = _toolParams as ToolCallParams['search_pathnames_only']
return `"${toolParams.queryStr}"`;
} else if (toolName === 'search_files') {
const toolParams = _toolParams as ToolCallParams['search_files']
} else if (toolName === 'search_for_files') {
const toolParams = _toolParams as ToolCallParams['search_for_files']
return `"${toolParams.queryStr}"`;
} else if (toolName === 'create_file_or_folder') {
const toolParams = _toolParams as ToolCallParams['create_file_or_folder']
@@ -1297,7 +1298,7 @@ const ToolRequestAcceptRejectButtons = () => {
</div>
)
return <div className="flex gap-2 my-1 items-center">
return <div className="flex gap-2 mx-4 items-center">
{approveButton}
{cancelButton}
{autoApproveToggle}
@@ -1343,26 +1344,41 @@ const EditToolChildren = ({ uri, changeDescription }: { uri: URI | undefined, ch
</div>
}
const LintErrorChildren = ({ lintErrors }: { lintErrors: LintErrorItem[] }) => {
return <div className="text-xs text-void-fg-4 opacity-80 border-l-2 border-void-warning px-2 py-0.5 flex flex-col gap-0.5 overflow-x-auto whitespace-nowrap">
{lintErrors.map((error, i) => (
<div key={i}>Lines {error.startLineNumber}-{error.endLineNumber}: {error.message}</div>
))}
</div>
}
const EditToolLintErrors = ({ lintErrors }: { lintErrors: LintErrorItem[] }) => {
if (lintErrors.length === 0) return null;
const [isOpen, setIsOpen] = useState(false);
return (
<div className="w-full px-2">
<ToolHeaderWrapper className='!border-t-0' title={'Lint errors'} desc1={''} isOpen={isOpen} onClick={() => { setIsOpen(o => !o) }} >
<div className="text-xs text-void-fg-4 opacity-80 border-l-2 border-void-warning px-2 py-0.5 flex flex-col gap-0.5 overflow-x-auto whitespace-nowrap">
<div className="w-full px-2 mt-0.5">
<div
className={`flex items-center cursor-pointer select-none transition-colors duration-150 pl-0 py-0.5 rounded group`}
onClick={() => setIsOpen(o => !o)}
style={{ background: 'none' }}
>
<ChevronRight
className={`mr-1 h-4 w-4 flex-shrink-0 transition-transform duration-100 text-void-fg-4 group-hover:text-void-fg-3 ${isOpen ? 'rotate-90' : ''}`}
/>
<span className="font-medium text-void-fg-4 group-hover:text-void-fg-3 text-xs">Lint errors</span>
</div>
<div
className={`overflow-hidden transition-all duration-200 ease-in-out ${isOpen ? 'opacity-100 py-1' : 'max-h-0 opacity-0'} text-xs pl-5`}
>
<div className="flex flex-col gap-0.5 overflow-x-auto whitespace-nowrap text-void-fg-4 opacity-90 border-l-2 border-void-warning px-2 py-0.5">
{lintErrors.map((error, i) => (
<div key={i}>Lines {error.startLineNumber}-{error.endLineNumber}: {error.message}</div>
<div key={i} className="">Lines {error.startLineNumber}-{error.endLineNumber}: {error.message}</div>
))}
</div>
</ToolHeaderWrapper>
</div>
</div>
)
);
}
@@ -1427,11 +1443,18 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
const isError = toolMessage.type === 'tool_error'
const componentParams: ToolHeaderParams = { title, desc1, isError, icon }
if (toolMessage.params.startLine !== null || toolMessage.params.endLine !== null) {
const start = toolMessage.params.startLine === null ? `1` : `${toolMessage.params.startLine}`
const end = toolMessage.params.endLine === null ? `` : `${toolMessage.params.endLine}`
const addStr = `(${start}-${end})`
componentParams.desc1 += ` ${addStr}`
}
if (toolMessage.type === 'success') {
const { params, result } = toolMessage
componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }
if (result.hasNextPage && params.pageNumber === 1) // first page
componentParams.desc2 = '(more content available)'
componentParams.desc2 = `(truncated after ${Math.round(MAX_FILE_CHARS_PAGE) / 1000}k)`
else if (params.pageNumber > 1) // subsequent pages
componentParams.desc2 = `(part ${params.pageNumber})`
}
@@ -1582,7 +1605,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
return <ToolHeaderWrapper {...componentParams} />
}
},
'search_files': {
'search_for_files': {
resultWrapper: ({ toolMessage }) => {
const accessor = useAccessor()
const commandService = accessor.get('ICommandService')
@@ -1626,6 +1649,47 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
}
},
'read_lint_errors': {
resultWrapper: ({ toolMessage }) => {
const accessor = useAccessor()
const commandService = accessor.get('ICommandService')
const title = getTitle(toolMessage)
const { uri } = toolMessage.params ?? {}
const desc1 = uri ? getBasename(uri.fsPath) : '';
const icon = null
if (toolMessage.type === 'tool_request') return null
if (toolMessage.type === 'rejected') return null // will never happen, not rejectable
if (toolMessage.type === 'running_now') return null // do not show running
const isError = toolMessage.type === 'tool_error'
const componentParams: ToolHeaderParams = { title, desc1, isError, icon }
if (toolMessage.type === 'success') {
const { params, result } = toolMessage
componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }
if (result.lintErrors)
componentParams.children = <LintErrorChildren lintErrors={result.lintErrors} />
else
componentParams.children = `No lint errors found.`
}
else if (toolMessage.type === 'tool_error') {
const { params, result } = toolMessage
if (params) componentParams.desc2 = <JumpToFileButton uri={params.uri} />
componentParams.children = <ToolChildrenWrapper>
<CodeChildren>
{result}
</CodeChildren>
</ToolChildrenWrapper>
}
return <ToolHeaderWrapper {...componentParams} />
},
},
// ---
'create_file_or_folder': {
@@ -1870,28 +1934,30 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
const Checkpoint = ({ message, threadId, messageIdx, isCheckpointGhost, threadIsRunning }: { message: CheckpointEntry, threadId: string; messageIdx: number, isCheckpointGhost: boolean, threadIsRunning: boolean }) => {
const accessor = useAccessor()
const chatThreadService = accessor.get('IChatThreadService')
const [showCheckpointIcon, setShowCheckpointIcon] = React.useState(false); // add icon state
return <div
className={`flex items-center justify-center px-2 `}
>
<div
className={`
text-xs
text-void-fg-3
cursor-pointer select-none
${isCheckpointGhost ? 'opacity-50' : 'opacity-100'}
`}
text-xs
text-void-fg-3
cursor-pointer select-none
${isCheckpointGhost ? 'opacity-50' : 'opacity-100'}
`}
style={{ position: 'relative', display: 'inline-block' }} // allow absolute icon
onClick={() => {
if (threadIsRunning) return
chatThreadService.jumpToCheckpointBeforeMessageIdx({ threadId, messageIdx, jumpToUserModified: true })
}}
>
Checkpoint
Checkpoint
</div>
</div>
}
type ChatBubbleMode = 'display' | 'edit'
type ChatBubbleProps = {
chatMessage: ChatMessage,
@@ -2438,7 +2504,6 @@ export const SidebarChat = () => {
role: 'assistant',
displayContent: displayContentSoFar ?? '',
reasoning: reasoningSoFar ?? '',
toolCall: toolCallSoFar,
anthropicReasoning: null,
}}
messageIdx={streamingChatIdx}
@@ -245,12 +245,12 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => {
// </button>
const acceptAllButton = <AcceptAllButtonWrapper
text={'Accept File'}
text={'Keep Changes'}
onClick={onAcceptAll}
/>
const rejectAllButton = <RejectAllButtonWrapper
text={'Reject File'}
text={'Reject All'}
onClick={onRejectAll}
/>
@@ -11,11 +11,12 @@ import { getModelCapabilities, ollamaRecommendedModels } from '../../../../commo
import { ChatMarkdownRender } from '../markdown/ChatMarkdownRender.js';
import { AddModelInputBox, AnimatedCheckmarkButton, ollamaSetupInstructions, OneClickSwitchButton, SettingsForProvider } from '../void-settings-tsx/Settings.js';
const OVERRIDE_VALUE = true
export const VoidOnboarding = () => {
const voidSettingsState = useSettingsState()
const isOnboardingComplete = voidSettingsState.globalSettings.isOnboardingComplete
const isOnboardingComplete = voidSettingsState.globalSettings.isOnboardingComplete || OVERRIDE_VALUE
const isDark = useIsDark()
@@ -23,6 +24,7 @@ export const VoidOnboarding = () => {
<div className={`@@void-scope ${isDark ? 'dark' : ''}`}>
<div
className={`
hidden
bg-void-bg-3 fixed top-0 right-0 bottom-0 left-0 width-full h-full z-[99999]
transition-all duration-1000 ${isOnboardingComplete ? 'opacity-0 pointer-events-none' : 'opacity-100 pointer-events-auto'}
`}
@@ -312,12 +312,11 @@ export const ModelDump = () => {
</div>
{/* right part is anything that fits */}
<div className='flex items-center gap-4'
data-tooltip-id='void-tooltip'
data-tooltip-place='top'
data-tooltip-content={disabled? `${displayInfoOfProviderName(providerName).title} is disabled`
: (isHidden ? `'${modelName}' won't appear in dropdowns` : ``)
}
// data-tooltip-id='void-tooltip'
// data-tooltip-place='top'
// data-tooltip-content={disabled ? `${displayInfoOfProviderName(providerName).title} is disabled`
// : (isHidden ? `'${modelName}' won't appear in dropdowns` : ``)
// }
>
<span className='opacity-50 truncate'>{isAutodetected ? '(detected locally)' : isDefault ? '' : '(custom model)'}</span>
@@ -616,7 +615,19 @@ export const FeaturesTab = () => {
{/* FIM */}
<div className='w-full'>
<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'>
{/* Enable Switch */}
@@ -696,7 +707,7 @@ export const FeaturesTab = () => {
value={voidSettingsState.globalSettings.includeToolLintErrors}
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>
@@ -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);
@@ -25,10 +25,37 @@ import { Disposable } from '../../../../base/common/lifecycle.js';
import { localize2 } from '../../../../nls.js';
import { StagingSelectionItem } from '../common/chatThreadServiceTypes.js';
import { IChatThreadService } from './chatThreadService.js';
import { getActiveWindow } from '../../../../base/browser/dom.js';
// ---------- 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' }) => {
if (!range)
return null
@@ -63,31 +90,6 @@ export const roundRangeToLines = (range: IRange | null | undefined, options: { e
// }
const findStagingItemToReplace = (currentSelections: StagingSelectionItem[] | undefined, newSelection: StagingSelectionItem): [number, StagingSelectionItem] | null => {
if (!currentSelections) return null
for (let i = 0; i < currentSelections.length; i += 1) {
const s = currentSelections[i]
if (s.uri.fsPath !== newSelection.uri.fsPath) continue
if (s.type === 'File' && newSelection.type === 'File') {
return [i, s] as const
}
if (s.type === 'CodeSelection' && newSelection.type === 'CodeSelection') {
if (s.uri.fsPath !== newSelection.uri.fsPath) continue
// if there's any collision return true
const [oldStart, oldEnd] = s.range
const [newStart, newEnd] = newSelection.range
if (oldStart !== newStart || oldEnd !== newEnd) continue
return [i, s] as const
}
if (s.type === 'Folder' && newSelection.type === 'Folder') {
return [i, s] as const
}
}
return null
}
const VOID_OPEN_SIDEBAR_ACTION_ID = 'void.sidebar.open'
registerAction2(class extends Action2 {
@@ -132,7 +134,7 @@ registerAction2(class extends Action2 {
}
const selection: StagingSelectionItem = !selectionRange || (selectionRange.startLineNumber > selectionRange.endLineNumber) ? {
const newSelection: StagingSelectionItem = !selectionRange || (selectionRange.startLineNumber > selectionRange.endLineNumber) ? {
type: 'File',
uri: model.uri,
language: model.getLanguageId(),
@@ -163,21 +165,17 @@ registerAction2(class extends Action2 {
}
// if matches with existing selection, overwrite (since text may change)
const replaceRes = findStagingItemToReplace(selections, selection)
if (replaceRes) {
const [idx, newSel] = replaceRes
if (idx !== undefined && idx !== -1) {
setSelections([
...selections!.slice(0, idx),
newSel,
...selections!.slice(idx + 1, Infinity)
])
}
const idx = findStagingSelectionIndex(selections, newSelection)
if (idx !== null && idx !== -1) {
setSelections([
...selections!.slice(0, idx),
newSelection,
...selections!.slice(idx + 1, Infinity)
])
}
// if no match, add it
else {
setSelections([...(selections ?? []), selection])
setSelections([...(selections ?? []), newSelection])
}
}
@@ -228,9 +226,13 @@ registerAction2(class extends Action2 {
metricsService.capture('Chat Navigation', { type: 'New Chat' })
stateService.setState({ isHistoryOpen: false, currentTab: 'chat' })
stateService.fireFocusChat()
const chatThreadService = accessor.get(IChatThreadService)
chatThreadService.openNewThread()
// focus
stateService.fireFocusChat()
const window = getActiveWindow()
window.requestAnimationFrame(() => stateService.fireFocusChat())
}
})
@@ -14,7 +14,7 @@ import { EndOfLinePreference } from '../../../../editor/common/model.js'
import { basename } from '../../../../base/common/path.js'
import { IVoidCommandBarService } from './voidCommandBarService.js'
import { computeDirectoryTree1Deep, IDirectoryStrService, stringifyDirectoryTree1Deep } from './directoryStrService.js'
import { IMarkerService } from '../../../../platform/markers/common/markers.js'
import { IMarkerService, MarkerSeverity } from '../../../../platform/markers/common/markers.js'
import { timeout } from '../../../../base/common/async.js'
import { RawToolParamsObj } from '../common/sendLLMMessageTypes.js'
import { ToolName } from '../common/prompt/prompts.js'
@@ -34,7 +34,7 @@ type ToolResultToString = { [T in ToolName]: (p: ToolCallParams[T], result: Awai
// pagination info
export const MAX_FILE_CHARS_PAGE = 50_000
export const MAX_FILE_CHARS_PAGE = Infinity
export const MAX_CHILDREN_URIs_PAGE = 500
export const MAX_TERMINAL_CHARS_PAGE = 20_000
export const TERMINAL_TIMEOUT_TIME = 5 // seconds
@@ -163,8 +163,11 @@ export class ToolsService implements IToolsService {
const uri = validateURI(uriStr)
const pageNumber = validatePageNum(pageNumberUnknown)
const startLine = validateNumber(startLineUnknown, { default: null })
const endLine = validateNumber(endLineUnknown, { default: null })
let startLine = validateNumber(startLineUnknown, { default: null })
let endLine = validateNumber(endLineUnknown, { default: null })
if (startLine !== null && startLine < 1) startLine = null
if (endLine !== null && endLine < 1) endLine = null
return { uri, startLine, endLine, pageNumber }
},
@@ -194,7 +197,7 @@ export class ToolsService implements IToolsService {
return { queryStr, searchInFolder, pageNumber }
},
search_files: (params: RawToolParamsObj) => {
search_for_files: (params: RawToolParamsObj) => {
const {
query: queryUnknown,
search_in_folder: searchInFolderUnknown,
@@ -276,8 +279,8 @@ export class ToolsService implements IToolsService {
const toIdx = MAX_FILE_CHARS_PAGE * pageNumber - 1
const fileContents = contents.slice(fromIdx, toIdx + 1) // paginate
const hasNextPage = (contents.length - 1) - toIdx >= 1
return { result: { fileContents, hasNextPage } }
const totalFileLen = contents.length
return { result: { fileContents, totalFileLen, hasNextPage } }
},
ls_dir: async ({ rootURI, pageNumber }) => {
@@ -308,7 +311,7 @@ export class ToolsService implements IToolsService {
return { result: { uris, hasNextPage } }
},
search_files: async ({ queryStr, isRegex, searchInFolder, pageNumber }) => {
search_for_files: async ({ queryStr, isRegex, searchInFolder, pageNumber }) => {
const searchFolders = searchInFolder === null ?
workspaceContextService.getWorkspace().folders.map(f => f.uri)
: [searchInFolder]
@@ -400,7 +403,7 @@ export class ToolsService implements IToolsService {
// given to the LLM after the call
this.stringOfResult = {
read_file: (params, result) => {
return result.fileContents + nextPageStr(result.hasNextPage)
return `${params.uri.fsPath}\n\`\`\`\n${result.fileContents}\n\`\`\`${nextPageStr(result.hasNextPage)}`
},
ls_dir: (params, result) => {
const dirTreeStr = stringifyDirectoryTree1Deep(params, result)
@@ -412,7 +415,7 @@ export class ToolsService implements IToolsService {
search_pathnames_only: (params, result) => {
return result.uris.map(uri => uri.fsPath).join('\n') + nextPageStr(result.hasNextPage)
},
search_files: (params, result) => {
search_for_files: (params, result) => {
return result.uris.map(uri => uri.fsPath).join('\n') + nextPageStr(result.hasNextPage)
},
read_lint_errors: (params, result) => {
@@ -471,9 +474,10 @@ export class ToolsService implements IToolsService {
private _getLintErrors(uri: URI): { lintErrors: LintErrorItem[] | null } {
const lintErrors = this.markerService
.read({ resource: uri })
.filter(l => l.severity === MarkerSeverity.Error || l.severity === MarkerSeverity.Warning)
.map(l => ({
code: typeof l.code === 'string' ? l.code : l.code?.value || '',
message: l.message,
message: (l.severity === MarkerSeverity.Error ? '(error) ' : '(warning) ') + l.message,
startLineNumber: l.startLineNumber,
endLineNumber: l.endLineNumber,
} satisfies LintErrorItem))
@@ -32,15 +32,12 @@ export class OnboardingContribution extends Disposable implements IWorkbenchCont
const onboardingContainer = h('div.void-onboarding-container').root;
workbench.appendChild(onboardingContainer);
// Mount the React component
this.instantiationService.invokeFunction((accessor: ServicesAccessor) => {
const result = mountVoidOnboarding(onboardingContainer, accessor);
if (result && typeof result.dispose === 'function') {
this._register(toDisposable(result.dispose));
}
});
// Register cleanup for the DOM element
this._register(toDisposable(() => {
if (onboardingContainer.parentElement) {
@@ -6,15 +6,17 @@
import { URI } from '../../../../base/common/uri.js';
import { VoidFileSnapshot } from './editCodeServiceTypes.js';
import { ToolName } from './prompt/prompts.js';
import { AnthropicReasoning, RawToolCallObj } from './sendLLMMessageTypes.js';
import { AnthropicReasoning, RawToolParamsObj } from './sendLLMMessageTypes.js';
import { ToolCallParams, ToolResultType } from './toolsServiceTypes.js';
export type ToolMessage<T extends ToolName> = {
role: 'tool';
content: string; // give this result to LLM (string of value)
id: string;
rawParams: RawToolParamsObj;
} & (
// in order of events:
| { type: 'invalid_params', result: null, name: T, params: RawToolCallObj | null, }
| { type: 'invalid_params', result: null, name: T, }
| { type: 'tool_request', result: null, name: T, params: ToolCallParams[T], } // params were validated, awaiting user
@@ -22,7 +24,7 @@ export type ToolMessage<T extends ToolName> = {
| { 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: 'rejected', result: null, name: T, params: ToolCallParams[T], }
| { type: 'rejected', result: null, name: T, params: ToolCallParams[T] }
) // user rejected
export type DecorativeCanceledTool = {
@@ -58,7 +60,6 @@ export type ChatMessage =
role: 'assistant';
displayContent: string; // content received from LLM - allowed to be '', will be replaced with (empty)
reasoning: string; // reasoning from the LLM, used for step-by-step thinking
toolCall: RawToolCallObj | undefined;
anthropicReasoning: AnthropicReasoning[] | null; // anthropic reasoning
}
@@ -51,11 +51,14 @@ export const defaultProviderSettings = {
export const defaultModelsOfProvider = {
openAI: [ // https://platform.openai.com/docs/models/gp
'gpt-4.1',
'gpt-4.1-mini',
'gpt-4.1-nano',
'o3-mini',
'o1',
'o1-mini',
'gpt-4o',
'gpt-4o-mini',
// 'o1',
// 'o1-mini',
// 'gpt-4o',
// 'gpt-4o-mini',
],
anthropic: [ // https://docs.anthropic.com/en/docs/about-claude/models
'claude-3-7-sonnet-latest',
@@ -127,6 +130,7 @@ export type VoidStaticModelInfo = { // not stateful
}
supportsSystemMessage: false | 'system-role' | 'developer-role' | 'separated'; // separated = anthropic where "system" is a special paramete
specialToolFormat?: 'openai-style' | 'anthropic-style', // null defaults to XML
supportsFIM: boolean;
reasoningCapabilities: false | {
@@ -344,12 +348,16 @@ const extensiveModelFallback: VoidStaticProviderInfo['modelOptionsFallback'] = (
if (lower.includes('quasar') || lower.includes('quaser')) return toFallback({ ...openSourceModelOptions_assumingOAICompat['quasar'] })
if (lower.includes('gpt') && lower.includes('mini') && (lower.includes('4.1') || lower.includes('4-1'))) return toFallback(openAIModelOptions['gpt-4.1-mini'])
if (lower.includes('gpt') && lower.includes('nano') && (lower.includes('4.1') || lower.includes('4-1'))) return toFallback(openAIModelOptions['gpt-4.1-nano'])
if (lower.includes('gpt') && (lower.includes('4.1') || lower.includes('4-1'))) return toFallback(openAIModelOptions['gpt-4.1'])
if (lower.includes('4o') && lower.includes('mini')) return toFallback(openAIModelOptions['gpt-4o-mini'])
if (lower.includes('4o')) return toFallback(openAIModelOptions['gpt-4o'])
if (lower.includes('o1') && lower.includes('mini')) return toFallback(openAIModelOptions['o1-mini'])
if (lower.includes('o1')) return toFallback(openAIModelOptions['o1'])
if (lower.includes('o3') && lower.includes('mini')) return toFallback(openAIModelOptions['o3-mini'])
// if (lower.includes('o3')) return toFallback(openAIModelOptions['o3'])
if (Object.keys(openSourceModelOptions_assumingOAICompat).map(k => k.toLowerCase()).includes(lower))
return toFallback(openSourceModelOptions_assumingOAICompat[lower as keyof typeof openSourceModelOptions_assumingOAICompat])
@@ -370,6 +378,7 @@ const anthropicModelOptions = {
cost: { input: 3.00, cache_read: 0.30, cache_write: 3.75, output: 15.00 },
downloadable: false,
supportsFIM: false,
specialToolFormat: 'anthropic-style',
supportsSystemMessage: 'separated',
reasoningCapabilities: {
supportsReasoning: true,
@@ -378,6 +387,7 @@ const anthropicModelOptions = {
reasoningMaxOutputTokens: 64_000, // can bump it to 128_000 with beta mode output-128k-2025-02-19
reasoningBudgetSlider: { type: 'slider', min: 1024, max: 32_000, default: 1024 }, // they recommend batching if max > 32_000
},
},
'claude-3-5-sonnet-20241022': {
contextWindow: 200_000,
@@ -385,6 +395,7 @@ const anthropicModelOptions = {
cost: { input: 3.00, cache_read: 0.30, cache_write: 3.75, output: 15.00 },
downloadable: false,
supportsFIM: false,
specialToolFormat: 'anthropic-style',
supportsSystemMessage: 'separated',
reasoningCapabilities: false,
},
@@ -394,6 +405,7 @@ const anthropicModelOptions = {
cost: { input: 0.80, cache_read: 0.08, cache_write: 1.00, output: 4.00 },
downloadable: false,
supportsFIM: false,
specialToolFormat: 'anthropic-style',
supportsSystemMessage: 'separated',
reasoningCapabilities: false,
},
@@ -403,6 +415,7 @@ const anthropicModelOptions = {
cost: { input: 15.00, cache_read: 1.50, cache_write: 18.75, output: 75.00 },
downloadable: false,
supportsFIM: false,
specialToolFormat: 'anthropic-style',
supportsSystemMessage: 'separated',
reasoningCapabilities: false,
},
@@ -411,6 +424,7 @@ const anthropicModelOptions = {
downloadable: false,
maxOutputTokens: 4_096,
supportsFIM: false,
specialToolFormat: 'anthropic-style',
supportsSystemMessage: 'separated',
reasoningCapabilities: false,
}
@@ -444,6 +458,36 @@ const anthropicSettings: VoidStaticProviderInfo = {
// ---------------- OPENAI ----------------
const openAIModelOptions = { // https://platform.openai.com/docs/pricing
'gpt-4.1': {
contextWindow: 1_047_576,
maxOutputTokens: 32_768,
cost: { input: 2.00, output: 8.00, cache_read: 0.50 },
downloadable: false,
supportsFIM: false,
specialToolFormat: 'openai-style',
supportsSystemMessage: 'developer-role',
reasoningCapabilities: false,
},
'gpt-4.1-mini': {
contextWindow: 1_047_576,
maxOutputTokens: 32_768,
cost: { input: 0.40, output: 1.60, cache_read: 0.10 },
downloadable: false,
supportsFIM: false,
specialToolFormat: 'openai-style',
supportsSystemMessage: 'developer-role',
reasoningCapabilities: false,
},
'gpt-4.1-nano': {
contextWindow: 1_047_576,
maxOutputTokens: 32_768,
cost: { input: 0.10, output: 0.40, cache_read: 0.03 },
downloadable: false,
supportsFIM: false,
specialToolFormat: 'openai-style',
supportsSystemMessage: 'developer-role',
reasoningCapabilities: false,
},
'o1': {
contextWindow: 128_000,
maxOutputTokens: 100_000,
@@ -468,6 +512,7 @@ const openAIModelOptions = { // https://platform.openai.com/docs/pricing
cost: { input: 2.50, cache_read: 1.25, output: 10.00, },
downloadable: false,
supportsFIM: false,
specialToolFormat: 'openai-style',
supportsSystemMessage: 'system-role',
reasoningCapabilities: false,
},
@@ -486,6 +531,7 @@ const openAIModelOptions = { // https://platform.openai.com/docs/pricing
cost: { input: 0.15, cache_read: 0.075, output: 0.60, },
downloadable: false,
supportsFIM: false,
specialToolFormat: 'openai-style',
supportsSystemMessage: 'system-role', // ??
reasoningCapabilities: false,
},
@@ -516,6 +562,15 @@ const xAIModelOptions = {
supportsSystemMessage: 'system-role',
reasoningCapabilities: false,
},
// 'grok-3': {
// contextWindow: 1_000_000,
// maxOutputTokens: null,
// cost: {},
// downloadable: false,
// supportsFIM: false,
// supportsSystemMessage: 'system-role',
// reasoningCapabilities: {canIOReasoning:false, canTurnOffReasoning:true,},
// }
} as const satisfies { [s: string]: VoidStaticModelInfo }
const xAISettings: VoidStaticProviderInfo = {
@@ -998,6 +1053,14 @@ export const getIsReasoningEnabledState = (
}
export const getMaxOutputTokens = (providerName: ProviderName, modelName: string, opts: { isReasoningEnabled: boolean }) => {
const {
reasoningCapabilities,
maxOutputTokens
} = getModelCapabilities(providerName, modelName)
return opts.isReasoningEnabled && reasoningCapabilities ? reasoningCapabilities.reasoningMaxOutputTokens : maxOutputTokens
}
// used to force reasoning state (complex) into something simple we can just read from when sending a message
export const getSendableReasoningInfo = (
featureName: FeatureName,
@@ -6,7 +6,7 @@
import { EndOfLinePreference } from '../../../../../editor/common/model.js';
import { StagingSelectionItem } from '../chatThreadServiceTypes.js';
import { os } from '../helpers/systemInfo.js';
import { RawToolCallObj } from '../sendLLMMessageTypes.js';
import { RawToolParamsObj } from '../sendLLMMessageTypes.js';
import { toolNamesThatRequireApproval } from '../toolsServiceTypes.js';
import { IVoidModelService } from '../voidModelService.js';
import { ChatMode } from '../voidSettingsTypes.js';
@@ -74,8 +74,8 @@ export const voidTools = {
description: `Returns full contents of a given file.`,
params: {
...uriParam('file'),
start_line: { description: 'Optional. Default is 1. Start reading on this line.' },
end_line: { description: 'Optional. Default is Infinity. Stop reading after this line.' },
start_line: { description: 'Optional. Only fill this in if you already know the line numbers you need to search. Defaults to 1.' },
end_line: { description: 'Optional. Only fill this in if you already know the line numbers you need to search. Defaults to Infinity.' },
...paginationParam,
},
},
@@ -84,7 +84,7 @@ export const voidTools = {
name: 'ls_dir',
description: `Lists all files and folders in the given URI.`,
params: {
...uriParam('folder'),
uri: { description: `Optional. The FULL path to the ${'folder'}. Leave this as empty or "" to search all folders.` },
...paginationParam,
},
},
@@ -106,19 +106,19 @@ export const voidTools = {
description: `Returns all pathnames that match a given query (searches ONLY file names). You should use this when looking for a file with a specific name or path.`,
params: {
query: { description: `Your query for the search.` },
search_in_folder: { description: 'Optional. Only search files in this given folder glob.' },
search_in_folder: { description: 'Optional. Only fill this in if you need to limit your search because there were too many results.' },
...paginationParam,
},
},
search_files: {
name: 'search_files',
description: `Returns all pathnames that match a given query (searches ONLY file contents). The query can be any substring or glob. You can follow this with read_file to view result contents.`,
search_for_files: {
name: 'search_for_files',
description: `Returns a list of file names whose content matches the given query. The query can be any substring or regex.`,
params: {
query: { description: `Your query for the search.` },
search_in_folder: { description: 'Optional. Only search files in this given folder glob.' },
search_in_folder: { description: 'Optional. Only fill this in if you need to limit your search because there were too many results.' },
is_regex: { description: 'Optional. Default is false. Whether query is a regex.' },
...paginationParam,
},
@@ -158,10 +158,9 @@ export const voidTools = {
...uriParam('file'),
change_description: {
description: `\
A brief code description of the change you want to make, with comments like "// ... existing code ..." to condense your writing. \
NEVER re-write the whole file. Instead, use comments like "// ... existing code ...". Bias towards writing as little as possible. \
Your description will be handed to a smaller model to make the change, so it must be clear and concise. \
Your description MUST be wrapped in triple backticks. \
A code description of the change you want to make, with comments like "// ... existing code ..." to condense your writing. \
NEVER re-write the whole file. Bias towards writing as little as possible. \
Here's an example of a good description:\n${editToolDescriptionExample}`
}
},
@@ -218,12 +217,11 @@ Format:
}).join('\n\n')}`
}
export const toolCallXMLStr = (toolCall: RawToolCallObj) => {
const t = toolCall
const params = Object.keys(t.rawParams).map(paramName => `<${paramName}>${t.rawParams[paramName as ToolParamName]}</${paramName}>`).join('\n')
export const toolCallXMLStr = (toolName: ToolName, toolParams: RawToolParamsObj) => {
const params = Object.keys(toolParams).map(paramName => `<${paramName}>${toolParams[paramName as ToolParamName]}</${paramName}>`).join('\n')
return `\
<${toolCall.name}>${!params ? '' : `\n${params}`}
</${toolCall.name}>`
<${toolName}>${!params ? '' : `\n${params}`}
</${toolName}>`
.replace('\t', ' ')
}
@@ -231,7 +229,7 @@ export const toolCallXMLStr = (toolCall: RawToolCallObj) => {
// - You are allowed to call multiple tools by specifying them consecutively. However, there should be NO text or writing between tool calls or after them.
const systemToolsXMLPrompt = (chatMode: ChatMode) => {
const tools = availableTools(chatMode)
if (!tools || tools.length === 0) return ''
if (!tools || tools.length === 0) return null
const toolXMLDefinitions = (`\
Available tools:
@@ -241,8 +239,8 @@ ${availableXMLToolsStr(tools)}`)
const toolCallXMLGuidelines = (`\
Tool calling details:
- Once you write a tool call, you must STOP and WAIT for the result.
- To call a tool, write its name and parameters in one of the XML formats specified above at the BOTTOM of your response.
- All parameters are REQUIRED unless noted otherwise.
- To call a tool, write its name and parameters in one of the XML formats specified above.
- You are only allowed to output ONE tool call, and it must be at the END of your response.
- Your tool call will be executed immediately, and the results will appear in the following user message.`)
@@ -255,7 +253,7 @@ ${toolCallXMLGuidelines}`
// ======================================================== chat (normal, gather, agent) ========================================================
export const chat_systemMessage = ({ workspaceFolders, openedURIs, activeURI, runningTerminalIds, directoryStr, chatMode: mode }: { workspaceFolders: string[], directoryStr: string, openedURIs: string[], activeURI: string | undefined, runningTerminalIds: string[], chatMode: ChatMode }) => {
export const chat_systemMessage = ({ workspaceFolders, openedURIs, activeURI, runningTerminalIds, directoryStr, chatMode: mode, includeXMLToolDefinitions }: { workspaceFolders: string[], directoryStr: string, openedURIs: string[], activeURI: string | undefined, runningTerminalIds: string[], chatMode: ChatMode, includeXMLToolDefinitions: boolean }) => {
const header = (`You are an expert coding ${mode === 'agent' ? 'agent' : 'assistant'} whose job is \
${mode === 'agent' ? `to help the user develop, run, and make changes to their codebase.`
: mode === 'gather' ? `to search, understand, and reference files in the user's codebase.`
@@ -289,7 +287,7 @@ ${directoryStr}
</files_overview>`)
const toolDefinitions = systemToolsXMLPrompt(mode)
const toolDefinitions = includeXMLToolDefinitions ? systemToolsXMLPrompt(mode) : null
const details: string[] = []
@@ -346,7 +344,6 @@ ${details.map((d, i) => `${i + 1}. ${d}`).join('\n\n')}`)
ansStrs.push(fsInfo)
if (toolDefinitions) ansStrs.push(toolDefinitions)
ansStrs.push(importantDetails)
ansStrs.push('Now, please assist the user with their query.')
const fullSystemMsgStr = ansStrs
.join('\n\n\n')
@@ -465,13 +462,13 @@ Output SEARCH/REPLACE blocks to edit the file according to the desired change. Y
Directions:
1. Your OUTPUT should consist ONLY of SEARCH/REPLACE blocks. Do NOT output any text or explanations before or after this.
2. The "ORIGINAL" code in each SEARCH/REPLACE block must EXACTLY match lines in the original file. This includes whitespace, comments, and other details.
3. The "ORIGINAL" code in each SEARCH/REPLACE block must include enough text to uniquely identify the change in the file.
2. The "ORIGINAL" code in each SEARCH/REPLACE block must EXACTLY match lines in the original file. The original code must NOT includes any new whitespace, comments, or any other modifications from the original code.
3. The "ORIGINAL" code in each SEARCH/REPLACE block must include enough text to uniquely identify the change in the file, but please bias towards writing as little as possible.
4. The "ORIGINAL" code in each SEARCH/REPLACE block must be disjoint from all other blocks.
The SEARCH/REPLACE blocks you generate will be applied immediately, and so they **MUST** produce a file that the user can run IMMEDIATELY.
- Make sure you add all necessary imports.
- Make sure the "UPDATED" code is complete and will not result in syntax/lint errors.
- Make sure the "UPDATED" code is ready for production as-is, and fix any relevant lint errors.
Follow coding conventions of the user (spaces, semilcolons, comments, etc). If the user spaces or formats things a certain way, CONTINUE formatting it that way, even if you prefer otherwise.
@@ -630,39 +627,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 ========================================================
@@ -13,7 +13,6 @@ import { generateUuid } from '../../../../base/common/uuid.js';
import { Event } from '../../../../base/common/event.js';
import { Disposable } from '../../../../base/common/lifecycle.js';
import { IVoidSettingsService } from './voidSettingsService.js';
// import { INotificationService } from '../../notification/common/notification.js';
// calls channel to implement features
export const ILLMMessageService = createDecorator<ILLMMessageService>('llmMessageService');
@@ -98,6 +97,7 @@ export class LLMMessageService extends Disposable implements ILLMMessageService
return null
}
const { settingsOfProvider, } = this.voidSettingsService.state
// add state for request id
const requestId = generateUuid();
@@ -106,13 +106,9 @@ export class LLMMessageService extends Disposable implements ILLMMessageService
this.llmMessageHooks.onError[requestId] = onError
this.llmMessageHooks.onAbort[requestId] = onAbort // used internally only
const { aiInstructions } = this.voidSettingsService.state.globalSettings
const { settingsOfProvider, } = this.voidSettingsService.state
// params will be stripped of all its functions over the IPC channel
this.channel.call('sendLLMMessage', {
...proxyParams,
aiInstructions,
requestId,
settingsOfProvider,
modelSelection,
@@ -27,16 +27,39 @@ export const getErrorMessage: (error: unknown) => string = (error) => {
}
export type LLMChatMessage = {
role: 'system';
content: string;
export type AnthropicLLMChatMessage = {
role: 'assistant',
content: string | (AnthropicReasoning | { type: 'text'; text: string }
| { type: 'tool_use'; name: string; input: Record<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;
} | {
role: 'assistant',
content: string; // text content
anthropicReasoning: AnthropicReasoning[] | null;
content: string | (AnthropicReasoning | { type: 'text'; text: string })[];
tool_calls?: { type: 'function'; id: string; function: { name: string; arguments: string; } }[];
} | {
role: 'tool',
content: string;
tool_call_id: string;
}
export type LLMChatMessage = AnthropicLLMChatMessage | OpenAILLMChatMessage
export type LLMFIMMessage = {
prefix: string;
suffix: string;
stopTokens: string[];
}
@@ -47,10 +70,10 @@ export type RawToolCallObj = {
name: ToolName;
rawParams: RawToolParamsObj;
doneParams: ToolParamName[];
id: string;
isDone: boolean;
};
export type AnthropicReasoning = ({ type: 'thinking'; thinking: any; signature: string; } | { type: 'redacted_thinking', data: any })
export type OnText = (p: { fullText: string; fullReasoning: string; toolCall?: RawToolCallObj }) => void
@@ -60,23 +83,18 @@ export type OnAbort = () => void
export type AbortRef = { current: (() => void) | null }
export type LLMFIMMessage = {
prefix: string;
suffix: string;
stopTokens: string[];
}
// service types
type SendLLMType = {
messagesType: 'chatMessages';
messages: LLMChatMessage[];
messages: LLMChatMessage[]; // the type of raw chat messages that we send to Anthropic, OAI, etc
separateSystemMessage: string | undefined;
chatMode: ChatMode | null;
} | {
messagesType: 'FIMMessage';
messages: LLMFIMMessage;
separateSystemMessage?: undefined;
chatMode?: undefined;
}
// service types
export type ServiceSendLLMMessageParams = {
onText: OnText;
onFinalMessage: OnFinalMessage;
@@ -95,8 +113,6 @@ export type SendLLMMessageParams = {
logging: { loggingName: string, loggingExtras?: { [k: string]: any } };
abortRef: AbortRef;
aiInstructions: string;
modelSelection: ModelSelection;
modelSelectionOptions: ModelSelectionOptions | undefined;
@@ -27,7 +27,7 @@ export type ToolCallParams = {
'ls_dir': { rootURI: URI, pageNumber: number },
'get_dir_structure': { rootURI: URI },
'search_pathnames_only': { queryStr: string, searchInFolder: string | null, pageNumber: number },
'search_files': { queryStr: string, isRegex: boolean, searchInFolder: URI | null, pageNumber: number },
'search_for_files': { queryStr: string, isRegex: boolean, searchInFolder: URI | null, pageNumber: number },
'read_lint_errors': { uri: URI },
// ---
'edit_file': { uri: URI, changeDescription: string },
@@ -39,11 +39,11 @@ export type ToolCallParams = {
// RESULT OF TOOL CALL
export type ToolResultType = {
'read_file': { fileContents: string, hasNextPage: boolean },
'read_file': { fileContents: string, totalFileLen: number, hasNextPage: boolean },
'ls_dir': { children: ShallowDirectoryItem[] | null, hasNextPage: boolean, hasPrevPage: boolean, itemsRemaining: number },
'get_dir_structure': { str: string, },
'search_pathnames_only': { uris: URI[], hasNextPage: boolean },
'search_files': { uris: URI[], hasNextPage: boolean },
'search_for_files': { uris: URI[], hasNextPage: boolean },
'read_lint_errors': { lintErrors: LintErrorItem[] | null },
// ---
'edit_file': Promise<{ lintErrors: LintErrorItem[] | null }>,
@@ -71,8 +71,8 @@ export interface IVoidSettingsService {
const _updatedModelsAfterDefaultModelsChange = (defaultModelNames: string[], options: { existingModels: VoidStatefulModelInfo[] }) => {
const { existingModels } = options
const _updatedModelsAfterDefaultModelsChange = (defaultModelNames: string[], options: { existingModels: VoidStatefulModelInfo[], didAutoDetect: boolean }) => {
const { existingModels, didAutoDetect } = options
const existingModelsMap: Record<string, VoidStatefulModelInfo> = {}
for (const existingModel of existingModels) {
@@ -82,7 +82,7 @@ const _updatedModelsAfterDefaultModelsChange = (defaultModelNames: string[], opt
const newDefaultModels = defaultModelNames.map((modelName, i) => ({
modelName,
isDefault: true,
isAutodetected: true,
isAutodetected: didAutoDetect,
isHidden: !!existingModelsMap[modelName]?.isHidden,
}))
@@ -101,7 +101,30 @@ export const modelFilterOfFeatureName: { [featureName in FeatureName]: { filter:
}
const _validatedModelState = (state: Omit<VoidSettingsState, '_modelOptions'>) => {
const _stateWithUpdatedDefaultModels = (state: VoidSettingsState): VoidSettingsState => {
let newSettingsOfProvider = state.settingsOfProvider
// recompute default models
for (const providerName of providerNames) {
const defaultModels = defaultSettingsOfProvider[providerName]?.models ?? []
const currentModels = newSettingsOfProvider[providerName]?.models ?? []
const defaultModelNames = defaultModels.map(m => m.modelName)
const newModels = _updatedModelsAfterDefaultModelsChange(defaultModelNames, { existingModels: currentModels, didAutoDetect: false })
newSettingsOfProvider = {
...newSettingsOfProvider,
[providerName]: {
...newSettingsOfProvider[providerName],
models: newModels,
},
}
}
return {
...state,
settingsOfProvider: newSettingsOfProvider,
}
}
const _validatedModelState = (state: Omit<VoidSettingsState, '_modelOptions'>): VoidSettingsState => {
let newSettingsOfProvider = state.settingsOfProvider
@@ -222,8 +245,10 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
}
// the stored data structure might be outdated, so we need to update it here
const finalState = readS
this.state = _validatedModelState(finalState);
this.state = readS
this.state = _stateWithUpdatedDefaultModels(this.state)
this.state = _validatedModelState(this.state);
this._resolver();
this._onDidChangeState.fire();
@@ -353,7 +378,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
const { models } = this.state.settingsOfProvider[providerName]
const oldModelNames = models.map(m => m.modelName)
const newModels = _updatedModelsAfterDefaultModelsChange(autodetectedModelNames, { existingModels: models })
const newModels = _updatedModelsAfterDefaultModelsChange(autodetectedModelNames, { existingModels: models, didAutoDetect: true })
this.setSettingOfProvider(providerName, 'models', newModels)
// if the models changed, log it
@@ -150,7 +150,7 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
providerName === 'groq' ? 'Get your [API Key here](https://console.groq.com/keys).' :
providerName === 'xAI' ? 'Get your [API Key here](https://console.x.ai).' :
providerName === 'mistral' ? 'Get your [API Key here](https://console.mistral.ai/api-keys).' :
providerName === 'openAICompatible' ? undefined :
providerName === 'openAICompatible' ? `Use any OpenAI-compatible endpoint (LM Studio, LiteLM, etc).` :
'',
isPasswordField: true,
}
@@ -383,7 +383,7 @@ export const defaultGlobalSettings: GlobalSettings = {
autoApprove: false,
showInlineSuggestions: true,
includeToolLintErrors: true,
isOnboardingComplete: true,
isOnboardingComplete: false,
}
export type GlobalSettingName = keyof GlobalSettings
@@ -3,6 +3,7 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { generateUuid } from '../../../../../base/common/uuid.js'
import { endsWithAnyPrefixOf, SurroundingsRemover } from '../../common/helpers/extractCodeFromResult.js'
import { availableTools, InternalToolInfo, ToolName, ToolParamName } from '../../common/prompt/prompts.js'
import { OnFinalMessage, OnText, RawToolCallObj, RawToolParamsObj } from '../../common/sendLLMMessageTypes.js'
@@ -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 }
const parseXMLPrefixToToolCall = (toolName: ToolName, str: string, toolOfToolName: ToolOfToolName): RawToolCallObj => {
const parseXMLPrefixToToolCall = (toolName: ToolName, toolId: string, str: string, toolOfToolName: ToolOfToolName): RawToolCallObj => {
const paramsObj: RawToolParamsObj = {}
const doneParams: ToolParamName[] = []
let isDone = false
@@ -179,7 +180,8 @@ const parseXMLPrefixToToolCall = (toolName: ToolName, str: string, toolOfToolNam
name: toolName,
rawParams: paramsObj,
doneParams: doneParams,
isDone: isDone
isDone: isDone,
id: toolId,
}
return ans
}
@@ -254,9 +256,11 @@ const parseXMLPrefixToToolCall = (toolName: ToolName, str: string, toolOfToolNam
}
}
export const extractToolsWrapper = (
onText: OnText, onFinalMessage: OnFinalMessage, chatMode: ChatMode
export const extractXMLToolsWrapper = (
onText: OnText, onFinalMessage: OnFinalMessage, chatMode: ChatMode | null
): { newOnText: OnText, newOnFinalMessage: OnFinalMessage } => {
if (!chatMode) return { newOnText: onText, newOnFinalMessage: onFinalMessage }
const tools = availableTools(chatMode)
if (!tools) return { newOnText: onText, newOnFinalMessage: onFinalMessage }
@@ -264,6 +268,8 @@ export const extractToolsWrapper = (
const toolOpenTags = tools.map(t => `<${t.name}>`)
for (const t of tools) { toolOfToolName[t.name] = t }
const toolId = generateUuid()
// detect <availableTools[0]></availableTools[0]>, etc
let fullText = '';
let trueFullText = ''
@@ -315,14 +321,12 @@ export const extractToolsWrapper = (
if (foundOpenTag !== null) {
latestToolCall = parseXMLPrefixToToolCall(
foundOpenTag.toolName,
toolId,
trueFullText.substring(foundOpenTag.idx, Infinity),
toolOfToolName,
)
}
onText({
...params,
fullText,
@@ -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"
}
}
}
*/
@@ -10,15 +10,14 @@ import { MistralCore } from '@mistralai/mistralai/core.js';
import { fimComplete } from '@mistralai/mistralai/funcs/fimComplete.js';
import { LLMChatMessage, LLMFIMMessage, ModelListParams, OllamaModelResponse, OnError, OnFinalMessage, OnText } from '../../common/sendLLMMessageTypes.js';
import { AnthropicLLMChatMessage, LLMChatMessage, LLMFIMMessage, ModelListParams, OllamaModelResponse, OnError, OnFinalMessage, OnText, RawToolCallObj, RawToolParamsObj } from '../../common/sendLLMMessageTypes.js';
import { ChatMode, displayInfoOfProviderName, ModelSelectionOptions, ProviderName, SettingsOfProvider } from '../../common/voidSettingsTypes.js';
import { prepareFIMMessage, prepareMessages } from './preprocessLLMMessages.js';
import { getSendableReasoningInfo, getModelCapabilities, getProviderCapabilities, defaultProviderSettings } from '../../common/modelCapabilities.js';
import { extractReasoningWrapper, extractToolsWrapper } from './extractGrammar.js';
import { getSendableReasoningInfo, getModelCapabilities, getProviderCapabilities, defaultProviderSettings, getMaxOutputTokens } from '../../common/modelCapabilities.js';
import { extractReasoningWrapper, extractXMLToolsWrapper } from './extractGrammar.js';
import { availableTools, InternalToolInfo, isAToolName, ToolParamName, voidTools } from '../../common/prompt/prompts.js';
type InternalCommonMessageParams = {
aiInstructions: string;
onText: OnText;
onFinalMessage: OnFinalMessage;
onError: OnError;
@@ -29,8 +28,8 @@ type InternalCommonMessageParams = {
_setAborter: (aborter: () => void) => void;
}
type SendChatParams_Internal = InternalCommonMessageParams & { messages: LLMChatMessage[]; chatMode: ChatMode | null; }
type SendFIMParams_Internal = InternalCommonMessageParams & { messages: LLMFIMMessage; }
type SendChatParams_Internal = InternalCommonMessageParams & { messages: LLMChatMessage[]; separateSystemMessage: string | undefined; chatMode: ChatMode | null; }
type SendFIMParams_Internal = InternalCommonMessageParams & { messages: LLMFIMMessage; separateSystemMessage: string | undefined; }
export type ListParams_Internal<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_)
if (!supportsFIM) {
if (modelName === modelName_)
@@ -106,16 +105,14 @@ const _sendOpenAICompatibleFIM = ({ messages: messages_, onFinalMessage, onError
return
}
const messages = prepareFIMMessage({ messages: messages_, aiInstructions, })
const openai = newOpenAICompatibleSDK({ providerName, settingsOfProvider })
openai.completions
.create({
model: modelName,
prompt: messages.prefix,
suffix: messages.suffix,
stop: messages.stopTokens,
max_tokens: messages.maxTokens,
prompt: prefix,
suffix: suffix,
stop: stopTokens,
max_tokens: 300,
})
.then(async response => {
const fullText = response.choices[0]?.text
@@ -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 {
modelName,
supportsSystemMessage,
contextWindow,
maxOutputTokens,
specialToolFormat,
reasoningCapabilities,
} = 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 includeInPayload = providerReasoningIOSettings?.input?.includeInPayload?.(reasoningInfo) || {}
// max tokens
const maxTokens = reasoningInfo?.isReasoningEnabled && reasoningCapabilities ? reasoningCapabilities.reasoningMaxOutputTokens : maxOutputTokens
// tools
const potentialTools = chatMode !== null ? openAITools(chatMode) : null
const nativeToolsObj = potentialTools ? { tools: potentialTools } as const : {}
// instance
const { messages } = prepareMessages({ messages: messages_, aiInstructions, supportsSystemMessage, supportsAnthropicReasoningSignature: false, contextWindow, maxOutputTokens: maxTokens })
const openai: OpenAI = newOpenAICompatibleSDK({ providerName, settingsOfProvider, includeInPayload })
const options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = {
model: modelName,
messages: messages,
messages: messages as any,
stream: true,
...nativeToolsObj,
// max_completion_tokens: maxTokens,
}
@@ -168,9 +213,9 @@ const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage
onFinalMessage = newOnFinalMessage
}
// manually parse out tool results
if (chatMode) {
const { newOnText, newOnFinalMessage } = extractToolsWrapper(onText, onFinalMessage, chatMode)
// manually parse out tool results if XML
if (!specialToolFormat) {
const { newOnText, newOnFinalMessage } = extractXMLToolsWrapper(onText, onFinalMessage, chatMode)
onText = newOnText
onFinalMessage = newOnFinalMessage
}
@@ -178,6 +223,10 @@ const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage
let fullReasoningSoFar = ''
let fullTextSoFar = ''
let toolName = ''
let toolId = ''
let toolParamsStr = ''
openai.chat.completions
.create(options)
.then(async response => {
@@ -188,6 +237,17 @@ const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage
const newText = chunk.choices[0]?.delta?.content ?? ''
fullTextSoFar += newText
// tool call
for (const tool of chunk.choices[0]?.delta?.tool_calls ?? []) {
const index = tool.index
if (index !== 0) continue
toolName += tool.function?.name ?? ''
toolParamsStr += tool.function?.arguments ?? '';
toolId += tool.id ?? ''
}
// reasoning
let newReasoning = ''
if (nameOfReasoningFieldInDelta) {
@@ -196,14 +256,21 @@ const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage
fullReasoningSoFar += newReasoning
}
onText({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar })
onText({
fullText: fullTextSoFar,
fullReasoning: fullReasoningSoFar,
toolCall: isAToolName(toolName) ? { name: toolName, rawParams: {}, isDone: false, doneParams: [], id: toolId } : undefined,
})
}
// on final
if (!fullTextSoFar && !fullReasoningSoFar) {
if (!fullTextSoFar && !fullReasoningSoFar && !toolName) {
onError({ message: 'Void: Response from model was empty.', fullError: null })
}
else {
onFinalMessage({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar, anthropicReasoning: null });
const toolCall = openAIToolToRawToolCallObj(toolName, toolParamsStr, toolId)
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)
@@ -251,14 +318,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 ------------
const sendAnthropicChat = ({ messages: messages_, providerName, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName: modelName_, _setAborter, aiInstructions, chatMode }: SendChatParams_Internal) => {
const sendAnthropicChat = ({ messages, providerName, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName: modelName_, _setAborter, separateSystemMessage, chatMode }: SendChatParams_Internal) => {
const {
modelName,
supportsSystemMessage,
contextWindow,
maxOutputTokens,
reasoningCapabilities,
specialToolFormat,
} = getModelCapabilities(providerName, modelName_)
const thisConfig = settingsOfProvider.anthropic
@@ -269,26 +370,32 @@ const sendAnthropicChat = ({ messages: messages_, providerName, onText, onFinalM
const includeInPayload = providerReasoningIOSettings?.input?.includeInPayload?.(reasoningInfo) || {}
// anthropic-specific - max tokens
const maxTokens = reasoningInfo?.isReasoningEnabled && reasoningCapabilities ? reasoningCapabilities.reasoningMaxOutputTokens : maxOutputTokens
const maxTokens = getMaxOutputTokens(providerName, modelName_, { isReasoningEnabled: !!reasoningInfo?.isReasoningEnabled })
// tools
const potentialTools = chatMode !== null ? anthropicTools(chatMode) : null
const nativeToolsObj = potentialTools ? { tools: potentialTools, tool_choice: { type: 'auto' } } as const : {}
// instance
const { messages, separateSystemMessageStr } = prepareMessages({ messages: messages_, aiInstructions, supportsSystemMessage, supportsAnthropicReasoningSignature: true, contextWindow, maxOutputTokens: maxTokens })
const anthropic = new Anthropic({
apiKey: thisConfig.apiKey,
dangerouslyAllowBrowser: true
});
const stream = anthropic.messages.stream({
system: separateSystemMessageStr,
messages: messages,
system: separateSystemMessage ?? undefined,
messages: messages as AnthropicLLMChatMessage[],
model: modelName,
max_tokens: maxTokens ?? 4_096, // anthropic requires this
...includeInPayload,
...nativeToolsObj,
})
// manually parse out tool results
if (chatMode) {
const { newOnText, newOnFinalMessage } = extractToolsWrapper(onText, onFinalMessage, chatMode)
if (!specialToolFormat) {
const { newOnText, newOnFinalMessage } = extractXMLToolsWrapper(onText, onFinalMessage, chatMode)
onText = newOnText
onFinalMessage = newOnFinalMessage
}
@@ -297,9 +404,17 @@ const sendAnthropicChat = ({ messages: messages_, providerName, onText, onFinalM
let fullText = ''
let fullReasoning = ''
// let fullToolName = ''
// let fullToolParams = ''
let fullToolName = ''
let fullToolParams = ''
const runOnText = () => {
onText({
fullText,
fullReasoning,
toolCall: isAToolName(fullToolName) ? { name: fullToolName, rawParams: {}, isDone: false, doneParams: [], id: 'dummy' } : undefined,
})
}
// there are no events for tool_use, it comes in at the end
stream.on('streamEvent', e => {
// start block
@@ -307,46 +422,49 @@ const sendAnthropicChat = ({ messages: messages_, providerName, onText, onFinalM
if (e.content_block.type === 'text') {
if (fullText) fullText += '\n\n' // starting a 2nd text block
fullText += e.content_block.text
onText({ fullText, fullReasoning, })
runOnText()
}
else if (e.content_block.type === 'thinking') {
if (fullReasoning) fullReasoning += '\n\n' // starting a 2nd reasoning block
fullReasoning += e.content_block.thinking
onText({ fullText, fullReasoning, })
runOnText()
}
else if (e.content_block.type === 'redacted_thinking') {
console.log('delta', e.content_block.type)
if (fullReasoning) fullReasoning += '\n\n' // starting a 2nd reasoning block
fullReasoning += '[redacted_thinking]'
onText({ fullText, fullReasoning, })
runOnText()
}
else if (e.content_block.type === 'tool_use') {
fullToolName += e.content_block.name ?? '' // anthropic gives us the tool name in the start block
runOnText()
}
// else if (e.content_block.type === 'tool_use') {
// fullToolName += e.content_block.name ?? '' // anthropic gives us the tool name in the start block
// onText({ fullText, fullReasoning, })
// }
}
// delta
else if (e.type === 'content_block_delta') {
if (e.delta.type === 'text_delta') {
fullText += e.delta.text
onText({ fullText, fullReasoning, })
runOnText()
}
else if (e.delta.type === 'thinking_delta') {
fullReasoning += e.delta.thinking
onText({ fullText, fullReasoning, })
runOnText()
}
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
runOnText()
}
// else if (e.delta.type === 'input_json_delta') { // tool use
// fullToolParams += e.delta.partial_json ?? '' // anthropic gives us the partial delta (string) here - https://docs.anthropic.com/en/api/messages-streaming
// onText({ fullText, fullReasoning, })
// }
}
})
// on done - (or when error/fail) - this is called AFTER last streamEvent
stream.on('finalMessage', (response) => {
const anthropicReasoning = response.content.filter(c => c.type === 'thinking' || c.type === 'redacted_thinking')
onFinalMessage({ fullText, fullReasoning, anthropicReasoning })
const tools = response.content.filter(c => c.type === 'tool_use')
const toolCall = tools[0] && anthropicToolToRawToolCallObj(tools[0])
const toolCallObj = toolCall ? { toolCall } : {}
onFinalMessage({ fullText, fullReasoning, anthropicReasoning, ...toolCallObj })
})
// on error
stream.on('error', (error) => {
@@ -360,7 +478,7 @@ const sendAnthropicChat = ({ messages: messages_, providerName, onText, onFinalM
// ------------ MISTRAL ------------
// https://docs.mistral.ai/api/#tag/fim
const sendMistralFIM = ({ messages: messages_, onFinalMessage, onError, settingsOfProvider, modelName: modelName_, _setAborter, providerName, aiInstructions, modelSelectionOptions }: SendFIMParams_Internal) => {
const sendMistralFIM = ({ messages, onFinalMessage, onError, settingsOfProvider, modelName: modelName_, _setAborter, providerName }: SendFIMParams_Internal) => {
const { modelName, supportsFIM } = getModelCapabilities(providerName, modelName_)
if (!supportsFIM) {
if (modelName === modelName_)
@@ -369,7 +487,6 @@ const sendMistralFIM = ({ messages: messages_, onFinalMessage, onError, settings
onError({ message: `Model ${modelName_} (${modelName}) does not support FIM.`, fullError: null })
return
}
const messages = prepareFIMMessage({ messages: messages_, aiInstructions })
const mistral = new MistralCore({ apiKey: settingsOfProvider.mistral.apiKey })
fimComplete(mistral,
@@ -378,7 +495,7 @@ const sendMistralFIM = ({ messages: messages_, onFinalMessage, onError, settings
prompt: messages.prefix,
suffix: messages.suffix,
stream: false,
maxTokens: messages.maxTokens,
maxTokens: 300,
stop: messages.stopTokens,
})
.then(async response => {
@@ -426,12 +543,10 @@ const ollamaList = async ({ onSuccess: onSuccess_, onError: onError_, settingsOf
}
}
const sendOllamaFIM = ({ messages: messages_, onFinalMessage, onError, settingsOfProvider, modelName, aiInstructions, _setAborter }: SendFIMParams_Internal) => {
const sendOllamaFIM = ({ messages, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }: SendFIMParams_Internal) => {
const thisConfig = settingsOfProvider.ollama
const ollama = newOllamaSDK({ endpoint: thisConfig.endpoint })
const messages = prepareFIMMessage({ messages: messages_, aiInstructions, })
let fullText = ''
ollama.generate({
model: modelName,
@@ -439,7 +554,7 @@ const sendOllamaFIM = ({ messages: messages_, onFinalMessage, onError, settingsO
suffix: messages.suffix,
options: {
stop: messages.stopTokens,
num_predict: messages.maxTokens, // max tokens
num_predict: 300, // max tokens
// repeat_penalty: 1,
},
raw: true,
@@ -11,7 +11,6 @@ import { sendLLMMessageToProviderImplementation } from './sendLLMMessage.impl.js
export const sendLLMMessage = ({
messagesType,
aiInstructions,
messages: messages_,
onText: onText_,
onFinalMessage: onFinalMessage_,
@@ -22,6 +21,7 @@ export const sendLLMMessage = ({
modelSelection,
modelSelectionOptions,
chatMode,
separateSystemMessage,
}: SendLLMMessageParams,
metricsService: IMetricsService
@@ -108,12 +108,12 @@ export const sendLLMMessage = ({
}
const { sendFIM, sendChat } = implementation
if (messagesType === 'chatMessages') {
sendChat({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName, _setAborter, providerName, aiInstructions, chatMode })
sendChat({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName, _setAborter, providerName, separateSystemMessage, chatMode })
return
}
if (messagesType === 'FIMMessage') {
if (sendFIM) {
sendFIM({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName, _setAborter, providerName, aiInstructions })
sendFIM({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName, _setAborter, providerName, separateSystemMessage })
return
}
onError({ message: `Error: This provider does not support Autocomplete yet.`, fullError: null })
@@ -47,7 +47,7 @@ export class VoidMainUpdateService extends Disposable implements IVoidUpdateServ
if (this._updateService.state.type === StateType.CheckingForUpdates) {
// Currently checking for updates
return { message: explicit ? 'Currently checking for updates...' : null } as const
return { message: explicit ? 'No updates found!' : null } as const
}
if (this._updateService.state.type === StateType.AvailableForDownload) {