Merge branch 'voideditor:main' into remote-wsl-extension

This commit is contained in:
Joaquin Coromina 2025-03-08 19:20:59 +07:00 committed by GitHub
commit 9fce7ce812
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 456 additions and 380 deletions

View file

@ -125,7 +125,7 @@ import { IMetricsService } from '../../workbench/contrib/void/common/metricsServ
import { IVoidUpdateService } from '../../workbench/contrib/void/common/voidUpdateService.js';
import { MetricsMainService } from '../../workbench/contrib/void/electron-main/metricsMainService.js';
import { VoidMainUpdateService } from '../../workbench/contrib/void/electron-main/voidUpdateMainService.js';
import { LLMMessageChannel } from '../../workbench/contrib/void/electron-main/llmMessageChannel.js';
import { LLMMessageChannel } from '../../workbench/contrib/void/electron-main/sendLLMMessageChannel.js';
/**
* The main VS Code application. There will only ever be one instance,
@ -1256,8 +1256,8 @@ export class CodeApplication extends Disposable {
const voidUpdatesChannel = ProxyChannel.fromService(accessor.get(IVoidUpdateService), disposables);
mainProcessElectronServer.registerChannel('void-channel-update', voidUpdatesChannel);
const llmMessageChannel = new LLMMessageChannel(accessor.get(IMetricsService));
mainProcessElectronServer.registerChannel('void-channel-llmMessageService', llmMessageChannel);
const sendLLMMessageChannel = new LLMMessageChannel(accessor.get(IMetricsService));
mainProcessElectronServer.registerChannel('void-channel-llmMessage', sendLLMMessageChannel);
// Extension Host Debug Broadcasting
const electronExtensionHostDebugBroadcastChannel = new ElectronExtensionHostDebugBroadcastChannel(accessor.get(IWindowsMainService));

View file

@ -17,8 +17,10 @@ import { EditorResourceAccessor } from '../../../common/editor.js';
import { IModelService } from '../../../../editor/common/services/model.js';
import { extractCodeFromRegular } from './helpers/extractCodeFromResult.js';
import { registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js';
import { ILLMMessageService } from '../common/llmMessageService.js';
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 { IContextGatheringService } from './contextGatheringService.js';
@ -767,8 +769,6 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
// console.log('B')
// create a new autocompletion and add it to cache
const newAutocompletion: Autocompletion = {
id: this._autocompletionId++,
@ -788,8 +788,14 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
console.log('starting autocomplete...', predictionType)
const featureName: FeatureName = 'Autocomplete'
const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName]
const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[modelSelection.providerName]?.[modelSelection.modelName] : undefined
const isEnabled = this._settingsService.state.globalSettings.enableAutocomplete
// set parameters of `newAutocompletion` appropriately
newAutocompletion.llmPromise = new Promise((resolve, reject) => {
newAutocompletion.llmPromise = isEnabled ? new Promise((resolve, reject) => reject('Autocomplete is disabled')) : new Promise((resolve, reject) => {
const requestId = this._llmMessageService.sendLLMMessage({
messagesType: 'FIMMessage',
@ -798,7 +804,8 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
suffix: llmSuffix,
stopTokens: stopTokens,
},
useProviderFor: 'Autocomplete',
modelSelection,
modelSelectionOptions,
logging: { loggingName: 'Autocomplete' },
onText: () => { }, // unused in FIMMessage
// onText: async ({ fullText, newText }) => {
@ -882,6 +889,7 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
@ILLMMessageService private readonly _llmMessageService: ILLMMessageService,
@IEditorService private readonly _editorService: IEditorService,
@IModelService private readonly _modelService: IModelService,
@IVoidSettingsService private readonly _settingsService: IVoidSettingsService,
// @IContextGatheringService private readonly _contextGatheringService: IContextGatheringService,
) {
super()

View file

@ -11,15 +11,16 @@ import { IStorageService, StorageScope, StorageTarget } from '../../../../platfo
import { URI } from '../../../../base/common/uri.js';
import { Emitter, Event } from '../../../../base/common/event.js';
import { IRange } from '../../../../editor/common/core/range.js';
import { ILLMMessageService } from '../common/llmMessageService.js';
import { ILLMMessageService } from '../common/sendLLMMessageService.js';
import { chat_userMessageContent, chat_systemMessage, chat_lastUserMessageWithFilesAdded, chat_selectionsString } from './prompt/prompts.js';
import { InternalToolInfo, IToolsService, ToolCallParams, ToolResultType, ToolName, toolNamesThatRequireApproval, voidTools } from './toolsService.js';
import { LLMChatMessage, toLLMChatMessage, ToolCallType } from '../common/llmMessageTypes.js';
import { AnthropicReasoning, LLMChatMessage, ToolCallType } from '../common/sendLLMMessageTypes.js';
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
import { IVoidFileService } from '../common/voidFileService.js';
import { generateUuid } from '../../../../base/common/uuid.js';
import { getErrorMessage } from '../../../../base/common/errors.js';
import { ChatMode } from '../common/voidSettingsTypes.js';
import { ChatMode, FeatureName } from '../common/voidSettingsTypes.js';
import { IVoidSettingsService } from '../common/voidSettingsService.js';
const findLastIndex = <T>(arr: T[], condition: (t: T) => boolean): number => {
@ -32,6 +33,28 @@ const findLastIndex = <T>(arr: T[], condition: (t: T) => boolean): number => {
}
const toLLMChatMessages = (chatMessages: ChatMessage[]): LLMChatMessage[] => {
const llmChatMessages: LLMChatMessage[] = []
for (const c of chatMessages) {
if (c.role === 'user') {
llmChatMessages.push({ role: c.role, content: c.content })
}
else if (c.role === 'assistant')
llmChatMessages.push({ role: c.role, content: c.content, anthropicReasoning: c.anthropicReasoning })
else if (c.role === 'tool')
llmChatMessages.push({ role: c.role, id: c.id, name: c.name, params: c.paramsStr, content: c.content })
else if (c.role === 'tool_request') {
// pass
}
else {
throw new Error(`Role ${(c as any).role} not recognized.`)
}
}
return llmChatMessages
}
// one of the square items that indicates a selection in a chat bubble (NOT a file, a Selection of text)
export type CodeSelection = {
type: 'Selection';
@ -73,9 +96,9 @@ export type ToolRequestApproval<T extends ToolName> = {
// WARNING: changing this format is a big deal!!!!!! need to migrate old format to new format on users' computers so people don't get errors.
export type ChatMessage =
{
| {
role: 'user';
content: string | null; // content displayed to the LLM on future calls - allowed to be '', will be replaced with (empty)
content: string; // content displayed to the LLM on future calls - allowed to be '', will be replaced with (empty)
displayContent: string | null; // content displayed to user - allowed to be '', will be ignored
selections: StagingSelectionItem[] | null; // the user's selection
state: {
@ -84,8 +107,10 @@ export type ChatMessage =
}
} | {
role: 'assistant';
content: string | null; // content received from LLM - allowed to be '', will be replaced with (empty)
reasoning: string | null; // reasoning from the LLM, used for step-by-step thinking
content: string; // content received from LLM - allowed to be '', will be replaced with (empty)
reasoning: string; // reasoning from the LLM, used for step-by-step thinking
anthropicReasoning: AnthropicReasoning[] | null; // anthropic reasoning
}
| ToolMessage<ToolName>
| ToolRequestApproval<ToolName>
@ -148,7 +173,11 @@ const newThreadObject = () => {
} satisfies ChatThreads[string]
}
export const THREAD_STORAGE_KEY = 'void.chatThreadStorage'
// past values:
// 'void.chatThreadStorage'
export const THREAD_STORAGE_KEY = 'void.chatThreadStorageI'
export interface IChatThreadService {
@ -213,6 +242,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
@ILLMMessageService private readonly _llmMessageService: ILLMMessageService,
@IToolsService private readonly _toolsService: IToolsService,
@IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService,
@IVoidSettingsService private readonly _settingsService: IVoidSettingsService,
) {
super()
this.state = { allThreads: {}, currentThreadId: null as unknown as string } // default state
@ -290,13 +320,6 @@ class ChatThreadService extends Disposable implements IChatThreadService {
// ---------- streaming ----------
private _finishStreamingTextMessage = (threadId: string, options: { content: string, reasoning?: string }, error?: { message: string, fullError: Error | null }) => {
// add assistant's message to chat history, and clear selection
this._addMessageToThread(threadId, { role: 'assistant', content: options.content, reasoning: options.reasoning || null })
this._setStreamState(threadId, { messageSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined, error })
}
async editUserMessageAndStreamResponse({ userMessage, chatMode, messageIdx }: { userMessage: string, chatMode: ChatMode, messageIdx: number }) {
@ -366,6 +389,12 @@ class ChatThreadService extends Disposable implements IChatThreadService {
: chatMode === 'agent' ? Object.keys(voidTools).map(toolName => voidTools[toolName as ToolName])
: undefined)
// these settings should not change throughout the loop (eg anthropic breaks if you change its thinking mode and it's using tools)
const featureName: FeatureName = 'Chat'
const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName]
const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[modelSelection.providerName]?.[modelSelection.modelName] : undefined
// agent loop
const agentLoop = async () => {
@ -384,7 +413,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
const awaitable = new Promise<void>((res, rej) => { res_ = res })
// replace last userMessage with userMessageFullContent (which contains all the files too)
const messages_ = this.getCurrentThread().messages.map(m => (toLLMChatMessage(m))).filter(m => !!m)
const messages_ = toLLMChatMessages(this.getCurrentThread().messages)
const lastUserMsgIdx = findLastIndex(messages_, m => m.role === 'user')
if (lastUserMsgIdx === -1) throw new Error(`Void: No user message found.`) // should never be -1
@ -396,24 +425,25 @@ class ChatThreadService extends Disposable implements IChatThreadService {
...messages_.slice(lastUserMsgIdx + 1, Infinity),
]
const llmCancelToken = this._llmMessageService.sendLLMMessage({
messagesType: 'chatMessages',
useProviderFor: 'Ctrl+L',
logging: { loggingName: `Agent` },
messages,
tools: tools,
modelSelection,
modelSelectionOptions,
logging: { loggingName: `Agent` },
onText: ({ fullText, fullReasoning }) => {
this._setStreamState(threadId, { messageSoFar: fullText, reasoningSoFar: fullReasoning })
},
onFinalMessage: async ({ fullText, toolCalls, fullReasoning }) => {
onFinalMessage: async ({ fullText, toolCalls, fullReasoning, anthropicReasoning }) => {
if ((toolCalls?.length ?? 0) === 0) {
this._finishStreamingTextMessage(threadId, { content: fullText, reasoning: fullReasoning })
this._addMessageToThread(threadId, { role: 'assistant', content: fullText, reasoning: fullReasoning, anthropicReasoning })
this._setStreamState(threadId, { messageSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined })
}
else {
this._addMessageToThread(threadId, { role: 'assistant', content: fullText, reasoning: fullReasoning || null })
this._addMessageToThread(threadId, { role: 'assistant', content: fullText, reasoning: fullReasoning, anthropicReasoning })
this._setStreamState(threadId, { messageSoFar: undefined, reasoningSoFar: undefined }) // clear streaming message
// deal with the tool
@ -428,14 +458,9 @@ class ChatThreadService extends Disposable implements IChatThreadService {
// 1. validate tool params
let toolParams: ToolCallParams[typeof toolName]
try {
console.log('A')
const params = await this._toolsService.validateParams[toolName](tool.paramsStr)
console.log('B')
toolParams = params
} catch (error) {
console.log('ERR1')
const errorMessage = getErrorMessage(error)
this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: tool.paramsStr, id: tool.id, content: errorMessage, result: { type: 'error', value: errorMessage }, })
res_()
@ -444,22 +469,16 @@ class ChatThreadService extends Disposable implements IChatThreadService {
// 2. if tool requires approval, await the approval
if (toolNamesThatRequireApproval.has(toolName)) {
console.log('C')
const voidToolId = generateUuid()
console.log('D')
const toolApprovalPromise = new Promise<void>((res, rej) => { this.resRejOfToolAwaitingApproval[voidToolId] = { res, rej } })
console.log('E')
this._addMessageToThread(threadId, { role: 'tool_request', name: toolName, params: toolParams, voidToolId: voidToolId })
try {
console.log('F')
await toolApprovalPromise
// accepted tool
}
catch (e) {
console.log('ERR2')
// TODO!!! test rejection
// if (Math.random() > 0) throw new Error('TESTING')
const errorMessage = 'Tool call was rejected by the user.'
this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: tool.paramsStr, id: tool.id, content: errorMessage, result: { type: 'error', value: errorMessage }, })
res_()
@ -470,10 +489,8 @@ class ChatThreadService extends Disposable implements IChatThreadService {
// 3. call the tool
let toolResult: ToolResultType[typeof toolName]
try {
console.log('G')
toolResult = await this._toolsService.callTool[toolName](toolParams as any) // typescript is so bad it doesn't even couple the type of ToolResult with the type of the function being called here
} catch (error) {
console.log('ERR3')
const errorMessage = getErrorMessage(error)
this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: tool.paramsStr, id: tool.id, content: errorMessage, result: { type: 'error', value: errorMessage }, })
res_()
@ -483,11 +500,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
// 4. stringify the result to give the LLM
let toolResultStr: string
try {
console.log('H')
toolResultStr = this._toolsService.stringOfResult[toolName](toolParams as any, toolResult as any)
// if (Math.random() > 0) throw new Error('This is not an allowed repo.')
} catch (error) {
const errorMessage = `Tool call succeeded, but there was an error stringifying the output.\n${getErrorMessage(error)}`
this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: tool.paramsStr, id: tool.id, content: errorMessage, result: { type: 'error', value: errorMessage }, })
@ -495,8 +508,6 @@ class ChatThreadService extends Disposable implements IChatThreadService {
return
}
console.log('I')
// 5. add to history
this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: tool.paramsStr, id: tool.id, content: toolResultStr, result: { type: 'success', params: toolParams, value: toolResult }, })
res_()
@ -506,20 +517,20 @@ class ChatThreadService extends Disposable implements IChatThreadService {
onError: (error) => {
const messageSoFar = this.streamState[threadId]?.messageSoFar ?? ''
const reasoningSoFar = this.streamState[threadId]?.reasoningSoFar ?? ''
this._finishStreamingTextMessage(threadId, { content: messageSoFar, reasoning: reasoningSoFar }, error)
// add assistant's message to chat history, and clear selection
this._addMessageToThread(threadId, { role: 'assistant', content: messageSoFar, reasoning: reasoningSoFar, anthropicReasoning: null })
this._setStreamState(threadId, { messageSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined, error })
res_()
},
})
if (llmCancelToken === null) break
this._setStreamState(threadId, { streamingToken: llmCancelToken })
console.log('awaiting agentloop')
await awaitable
console.log('done')
}
}
agentLoop() // DO NOT AWAIT THIS, add fn should resolve when we've added message (this lets us interrupt the agent loop correctly instead of waiting for it to resolve)
agentLoop()
}
@ -528,7 +539,8 @@ class ChatThreadService extends Disposable implements IChatThreadService {
if (llmCancelToken !== undefined) this._llmMessageService.abort(llmCancelToken)
const messageSoFar = this.streamState[threadId]?.messageSoFar ?? ''
const reasoningSoFar = this.streamState[threadId]?.reasoningSoFar ?? ''
this._finishStreamingTextMessage(threadId, { content: messageSoFar, reasoning: reasoningSoFar })
this._addMessageToThread(threadId, { role: 'assistant', content: messageSoFar, reasoning: reasoningSoFar, anthropicReasoning: null })
this._setStreamState(threadId, { messageSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined })
}
dismissStreamError(threadId: string): void {

View file

@ -38,11 +38,13 @@ import { EditorOption } from '../../../../editor/common/config/editorOptions.js'
import { Emitter } from '../../../../base/common/event.js';
import { VOID_OPEN_SETTINGS_ACTION_ID } from './voidSettingsPane.js';
import { ICommandService } from '../../../../platform/commands/common/commands.js';
import { ILLMMessageService } from '../common/llmMessageService.js';
import { LLMChatMessage, OnError, errorDetails } from '../common/llmMessageTypes.js';
import { ILLMMessageService } from '../common/sendLLMMessageService.js';
import { LLMChatMessage, OnError, errorDetails } from '../common/sendLLMMessageTypes.js';
import { IMetricsService } from '../common/metricsService.js';
import { IVoidFileService } from '../common/voidFileService.js';
import { IEditCodeService, URIStreamState, AddCtrlKOpts, StartApplyingOpts } from './editCodeServiceInterface.js';
import { IVoidSettingsService } from '../common/voidSettingsService.js';
import { FeatureName } from '../common/voidSettingsTypes.js';
const configOfBG = (color: Color) => {
return { dark: color, light: color, hcDark: color, hcLight: color, }
@ -268,6 +270,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
@INotificationService private readonly _notificationService: INotificationService,
@ICommandService private readonly _commandService: ICommandService,
@IVoidFileService private readonly _voidFileService: IVoidFileService,
@IVoidSettingsService private readonly _settingsService: IVoidSettingsService,
) {
super();
@ -1188,20 +1191,13 @@ class EditCodeService extends Disposable implements IEditCodeService {
// throws if there's an error
public startApplying(opts: StartApplyingOpts): [URI, Promise<void>] | null {
if (opts.type === 'rewrite') {
const added = this._initializeWriteoverStream(opts)
if (!added) return null
const [diffZone, promise] = added
return [diffZone._URI, promise]
}
else if (opts.type === 'searchReplace') {
const added = this._initializeSearchAndReplaceStream(opts)
if (!added) return null
if (!added) return null
const [diffZone, promise] = added
return [diffZone._URI, promise]
}
return null
let res: [DiffZone, Promise<void>] | undefined = undefined
if (opts.type === 'rewrite') res = this._initializeWriteoverStream(opts)
else if (opts.type === 'searchReplace') res = this._initializeSearchAndReplaceStream(opts)
if (!res) return null
const [diffZone, applyDonePromise] = res
return [diffZone._URI, applyDonePromise]
}
@ -1377,6 +1373,10 @@ class EditCodeService extends Disposable implements IEditCodeService {
let fullTextSoFar = '' // so far (INCLUDING ignored suffix)
let prevIgnoredSuffix = ''
const featureName: FeatureName = opts.from === 'ClickApply' ? 'Apply' : 'Ctrl+K'
const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName]
const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[modelSelection.providerName]?.[modelSelection.modelName] : undefined
const writeover = async () => {
let resMessageDonePromise: () => void = () => { }
@ -1384,9 +1384,10 @@ class EditCodeService extends Disposable implements IEditCodeService {
streamRequestIdRef.current = this._llmMessageService.sendLLMMessage({
messagesType: 'chatMessages',
useProviderFor: opts.from === 'ClickApply' ? 'Apply' : 'Ctrl+K',
logging: { loggingName: `Edit (Writeover) - ${from}` },
messages,
modelSelection,
modelSelectionOptions,
onText: (params) => {
const { fullText: fullText_ } = params
const newText_ = fullText_.substring(fullTextSoFar.length, Infinity)
@ -1471,13 +1472,13 @@ class EditCodeService extends Disposable implements IEditCodeService {
// promise that resolves when the apply is done
let resApplyPromise: () => void
let rejApplyPromise: (e: any) => void
const applyPromise = new Promise<void>((res_, rej_) => { resApplyPromise = res_; rejApplyPromise = rej_ })
let resApplyDonePromise: () => void
let rejApplyDonePromise: (e: any) => void
const applyDonePromise = new Promise<void>((res_, rej_) => { resApplyDonePromise = res_; rejApplyDonePromise = rej_ })
// add to history
const { onFinishEdit } = this._addToHistory(uri, {
onUndo: () => { if (diffZone._streamState.isStreaming) rejApplyPromise(new Error('Edit was interrupted by pressing undo.')) }
onUndo: () => { if (diffZone._streamState.isStreaming) rejApplyDonePromise(new Error('Edit was interrupted by pressing undo.')) }
})
// TODO replace these with whatever block we're on initially if already started (caching apply)
@ -1559,6 +1560,12 @@ class EditCodeService extends Disposable implements IEditCodeService {
let oldBlocks: ExtractedSearchReplaceBlock[] = []
const featureName: FeatureName = 'Apply'
const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName]
const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[modelSelection.providerName]?.[modelSelection.modelName] : undefined
const retryLoop = async () => {
// this generates >>>>>>> ORIGINAL <<<<<<< REPLACE blocks and and simultaneously applies it
let shouldSendAnotherMessage = true
@ -1569,13 +1576,14 @@ class EditCodeService extends Disposable implements IEditCodeService {
nMessagesSent += 1
let resMessageDonePromise: () => void = () => { }
const messageDonePromise = new Promise<void>((res_) => { resMessageDonePromise = res_ })
const messageDonePromise = new Promise<void>((res, rej) => { resMessageDonePromise = res })
streamRequestIdRef.current = this._llmMessageService.sendLLMMessage({
messagesType: 'chatMessages',
useProviderFor: 'Apply',
logging: { loggingName: `Edit (Search/Replace) - ${from}` },
messages,
modelSelection,
modelSelectionOptions,
onText: (params) => {
const { fullText } = params
// blocks are [done done done ... {writingFinal|writingOriginal}]
@ -1613,11 +1621,10 @@ class EditCodeService extends Disposable implements IEditCodeService {
if (typeof originalBounds === 'string') {
const content = errMsgOfInvalidStr(originalBounds, block.orig)
messages.push(
{ role: 'assistant', content: fullText }, // latest output
{ role: 'assistant', content: fullText, anthropicReasoning: null }, // latest output
{ role: 'user', content: content } // user explanation of what's wrong
)
if (streamRequestIdRef.current) this._llmMessageService.abort(streamRequestIdRef.current)
// REVERT
const numLines = this._getNumLines(uri)
@ -1638,7 +1645,9 @@ class EditCodeService extends Disposable implements IEditCodeService {
oldBlocks = []
addedTrackingZoneOfBlockNum.splice(0, Infinity) // clear the array
// abort and resolve
shouldSendAnotherMessage = true
if (streamRequestIdRef.current) this._llmMessageService.abort(streamRequestIdRef.current)
this._refreshStylesAndDiffsInURI(uri)
resMessageDonePromise()
return
@ -1750,9 +1759,9 @@ class EditCodeService extends Disposable implements IEditCodeService {
} // end retryLoop
retryLoop().then(() => resApplyPromise()).catch((e) => rejApplyPromise(e))
retryLoop().then(() => resApplyDonePromise()).catch((e) => rejApplyDonePromise(e))
return [diffZone, applyPromise]
return [diffZone, applyDonePromise]
}

View file

@ -3,7 +3,7 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { OnText } from '../../common/llmMessageTypes.js'
import { OnText } from '../../common/sendLLMMessageTypes.js'
import { DIVIDER, FINAL, ORIGINAL } from '../prompt/prompts.js'
class SurroundingsRemover {

View file

@ -17,41 +17,46 @@ export const tripleTick = ['```', '```']
export const editToolDesc_toolDescription = `\
A high level description of the change you'd like to make in the file. This description will be handed to a dumber, faster model that will quickly apply the change. \
Typically the best description you can give here is a high level view of the final code you'd like to see. For example, code excerpt(s) with "// ... existing code ..." comments to help you write less. \
Typically the best description you can give here is a high level view of the final code you'd like to see. For example, you can write code excerpt(s) with "// ... existing code ..." comments to help you write less. \
However, you are allowed to describe the change using whatever text/language you like, especially if the change is better described without code. \
Do NOT output the whole file if possible, and try to write as LITTLE as needed to describe the change.`
export const chat_systemMessage = (workspaces: string[], mode: 'agent' | 'gather' | 'chat') => `\
You are a coding ${mode === 'agent' ? 'agent' : 'assistant'}. Your job is to help the user understand/${mode === 'agent' ? 'make' : 'suggest'} changes to their codebase.
You will be given instructions to follow from the user, \`INSTRUCTIONS\`. You may also be given a list of selections that the user has specifically selected, \`SELECTIONS\`.
You are a coding ${mode === 'agent' ? 'agent' : 'assistant'}. Your job is to help the user ${mode === 'agent' ? 'make changes to their codebase' : 'search and understand their codebase'}.
You will be given instructions to follow from the user, \`INSTRUCTIONS\`. You may also be given a list of files that the user has specifically selected, \`SELECTIONS\`.
Please assist the user with their query. The user's query is never invalid.
The user's system information is as follows:
- ${os}
- Open workspaces: ${workspaces.join(', ')}
${mode === 'agent' || mode === 'gather' ? `\
${mode === 'agent' || mode === 'gather' /* tool use */ ? `\
You will be given tools you can call.
- Only use tools if they help you accomplish the user's goal. If the user simply says hi or asks you a question that you can answer without tools, then do NOT tools.
- If you think you should use tools given the user's request, you can use them without asking for permission. Feel free to use tools to gather context, make suggestions, ${mode === 'agent' ? 'edit files, ' : ''}etc.
- NEVER refer to a tool by name when speaking with the user. For example, do NOT say to the user user "I'm going to use \`list_dir\`". Instead, say "I'm going to list all files in ___ directory", etc. Do not refer to "pages" of results, just say you're getting more results.
- If you think you should use tools given the user's request, you can use them without asking for permission. Feel free to use tools to gather context, understand the codebase, ${mode === 'agent' ? 'edit files, ' : ''}etc.
- NEVER refer to a tool by name when speaking with the user. For example, do NOT say to the user "I'm going to use \`list_dir\`". Instead, say "I'm going to list all files in ___ directory", etc. Do not refer to "pages" of results, just say you're getting more results.
- Some tools only work if the user has a workspace open.
\
`: `\
You're allowed to ask for more context. For example, if the user only gives you a selection but you want to see the the full file, you can ask them to provide it.
\
`}
If you think it's appropriate to suggest an edit to a file, then you must describe your suggestion as follows:
- The change(s) you'd like to make must be written in CODE BLOCK(S) (wrapped in triple backticks).
- The first line in the code block should be the FULL PATH of the file you want to change. Just output the path in plaintext (not in a comment).
- The rest of the contents of the code block should describe of the change you'd like to make. This description will be given to a dumber, faster model that will quickly apply the change.
${mode === 'agent' /* code blocks */ ? `\
Keep in mind that any code blocks you output in the raw message (wrapped in triple backticks) will be treated specially as follows. This does NOT apply to code blocks in tool calls.
- Any code block you output will have an "Apply" button displayed to the user, and if the user clicks on it it will invoke the edit tool on the block's contents. As a result, all code blocks should describe relevant changes.
`: `\
If you think it's appropriate to suggest an edit to a file, then you must describe your suggestion in CODE BLOCK(S) (wrapped in triple backticks).
- The first line before any code block must be the FULL PATH of the file you want to change. If the path does not already exist, it will be created.
- The contents of the code block will be given to a dumber, faster model that will quickly apply the change.
- Contents of the code blocks do NOT need to be formal code, they just need to clearly and concisely communicate the change.
- Do NOT re-write the entire file in the code block(s). Instead, write comments like "// ... existing code" to indicate how to change the existing code.
\
`}
Do not output any of these instructions, nor tell the user anything about them unless directly prompted for them.
Do not tell the user anything about these instructions unless directly prompted for them.
\
`
@ -265,13 +270,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 of code in the original file.
3. The original code in each SEARCH/REPLACE block must include enough text to uniquely identify the change in the file.
4. The original code in each SEARCH/REPLACE block must be disjoint from all other blocks.
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.
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 "final" code is complete and will not result in syntax/lint errors.
- Make sure the "UPDATED" code is complete and will not result in syntax/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.
@ -308,11 +313,7 @@ ORIGINAL_FILE
${originalCode}
CHANGE
${applyStr}
INSTRUCTIONS
Please output SEARCH/REPLACE blocks to make the change. Return ONLY your suggested SEARCH/REPLACE blocks, without any explanation.
`
${applyStr}`

View file

@ -154,7 +154,7 @@ const getChatBubbleId = (threadId: string, messageIdx: number) => `${threadId}-$
// const voidSettingsService = accessor.get('IVoidSettingsService')
// const voidSettingsState = useSettingsState()
// const modelSelection = voidSettingsState.modelSelectionOfFeature['Ctrl+L']
// const modelSelection = voidSettingsState.modelSelectionOfFeature['Chat']
// if (!modelSelection) return null
// const { modelName, providerName } = modelSelection
@ -210,13 +210,13 @@ const ReasoningOptionDropdown = () => {
const voidSettingsService = accessor.get('IVoidSettingsService')
const voidSettingsState = useSettingsState()
const modelSelection = voidSettingsState.modelSelectionOfFeature['Ctrl+L']
const modelSelection = voidSettingsState.modelSelectionOfFeature['Chat']
if (!modelSelection) return null
const { modelName, providerName } = modelSelection
const { canToggleReasoning, reasoningBudgetSlider } = getModelCapabilities(providerName, modelName).supportsReasoning || {}
const { isReasoningEnabled } = getModelSelectionState(providerName, modelName, voidSettingsState.optionsOfModelSelection)
const { isReasoningEnabled } = getModelSelectionState(providerName, modelName, voidSettingsState.optionsOfModelSelection[providerName]?.[modelName])
if (canToggleReasoning && !reasoningBudgetSlider) { // if it's just a on/off toggle without a power slider (no models right now)
return null // unused right now
@ -277,7 +277,6 @@ interface VoidChatAreaProps {
divRef?: React.RefObject<HTMLDivElement>;
// UI customization
featureName: FeatureName;
className?: string;
showModelDropdown?: boolean;
showSelections?: boolean;
@ -304,7 +303,6 @@ export const VoidChatArea: React.FC<VoidChatAreaProps> = ({
isDisabled = false,
className = '',
showModelDropdown = true,
featureName,
showSelections = false,
showProspectiveSelections = true,
selections,
@ -363,7 +361,7 @@ export const VoidChatArea: React.FC<VoidChatAreaProps> = ({
{showModelDropdown && (
<div className='max-w-[200px] flex-grow'>
<ReasoningOptionDropdown />
<ModelDropdown featureName={featureName} />
<ModelDropdown featureName={'Chat'} />
</div>
)}
@ -727,8 +725,6 @@ const DropdownComponent = ({
const UserMessageComponent = ({ chatMessage, messageIdx, isLoading }: ChatBubbleProps & { chatMessage: ChatMessage & { role: 'user' } }) => {
const role = chatMessage.role
const accessor = useAccessor()
const chatThreadsService = accessor.get('IChatThreadService')
@ -758,7 +754,7 @@ const UserMessageComponent = ({ chatMessage, messageIdx, isLoading }: ChatBubble
const _mustInitialize = useRef(true)
const _justEnabledEdit = useRef(false)
useEffect(() => {
const canInitialize = role === 'user' && mode === 'edit' && textAreaRefState
const canInitialize = mode === 'edit' && textAreaRefState
const shouldInitialize = _justEnabledEdit.current || _mustInitialize.current
if (canInitialize && shouldInitialize) {
setStagingSelections(chatMessage.selections || [])
@ -771,7 +767,7 @@ const UserMessageComponent = ({ chatMessage, messageIdx, isLoading }: ChatBubble
_mustInitialize.current = false
}
}, [chatMessage, role, mode, _justEnabledEdit, textAreaRefState, textAreaFnsRef.current, _justEnabledEdit.current, _mustInitialize.current])
}, [chatMessage, mode, _justEnabledEdit, textAreaRefState, textAreaFnsRef.current, _justEnabledEdit.current, _mustInitialize.current])
const onOpenEdit = () => {
setIsBeingEdited(true)
@ -843,7 +839,6 @@ const UserMessageComponent = ({ chatMessage, messageIdx, isLoading }: ChatBubble
isDisabled={isDisabled}
showSelections={true}
showProspectiveSelections={false}
featureName="Ctrl+L"
selections={stagingSelections}
setSelections={setStagingSelections}
>
@ -893,7 +888,7 @@ const UserMessageComponent = ({ chatMessage, messageIdx, isLoading }: ChatBubble
</div>
{role === 'user' && <EditSymbol
{<EditSymbol
size={18}
className={`
absolute -top-1 -right-1
@ -931,11 +926,15 @@ const AssistantMessageComponent = ({ chatMessage, isLoading, messageIdx }: ChatB
const hasReasoning = !!reasoningStr
const thread = chatThreadsService.getCurrentThread()
const chatMessageLocation: ChatMessageLocation = {
threadId: thread.id,
messageIdx: messageIdx,
}
const isEmpty = !chatMessage.content && !chatMessage.reasoning
if (isEmpty) return null
return <>
{/* reasoning token */}
@ -1034,9 +1033,13 @@ const toolNameToComponent: { [T in ToolName]: {
} } = {
'read_file': {
requestWrapper: ({ toolRequest }) => {
const accessor = useAccessor()
const commandService = accessor.get('ICommandService')
const title = toolNameToTitle[toolRequest.name]
const { params } = toolRequest
return <DropdownComponent title={title} desc1={getBasename(params.uri.toString())} icon={<Dot className={`stroke-orange-500`} />} />
return <DropdownComponent title={title} desc1={getBasename(params.uri.toString())} icon={<Dot className={`stroke-orange-500`} />}
onClick={() => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }}
/>
},
resultWrapper: ({ toolMessage }) => {
const accessor = useAccessor()
@ -1198,9 +1201,13 @@ const toolNameToComponent: { [T in ToolName]: {
},
'delete_uri': {
requestWrapper: ({ toolRequest }) => {
const accessor = useAccessor()
const commandService = accessor.get('ICommandService')
const title = toolNameToTitle[toolRequest.name]
const { params } = toolRequest
return <DropdownComponent title={title} desc1={getBasename(params.uri.fsPath) + ' (deleted)'} />
return <DropdownComponent title={title} desc1={getBasename(params.uri.fsPath) + ' (deleted)'}
onClick={() => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }}
/>
},
resultWrapper: ({ toolMessage }) => {
const accessor = useAccessor()
@ -1218,9 +1225,15 @@ const toolNameToComponent: { [T in ToolName]: {
},
'edit': {
requestWrapper: ({ toolRequest }) => {
const accessor = useAccessor()
const commandService = accessor.get('ICommandService')
const title = toolNameToTitle[toolRequest.name]
const { params } = toolRequest
return <DropdownComponent title={title} desc1={getBasename(params.uri.fsPath)} icon={<Dot className={`stroke-orange-500`} />} />
return <DropdownComponent title={title} desc1={getBasename(params.uri.fsPath)} icon={<Dot className={`stroke-orange-500`} />}
onClick={() => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }}
>
<ChatMarkdownRender string={params.changeDescription} />
</DropdownComponent>
},
resultWrapper: ({ toolMessage }) => {
const accessor = useAccessor()
@ -1240,9 +1253,13 @@ const toolNameToComponent: { [T in ToolName]: {
},
'terminal_command': {
requestWrapper: ({ toolRequest }) => {
const accessor = useAccessor()
const commandService = accessor.get('ICommandService')
const title = toolNameToTitle[toolRequest.name]
const { params } = toolRequest
return <DropdownComponent title={title} desc1={`"${params.command}"`} icon={<Dot className={`stroke-orange-500`} />} />
return <DropdownComponent title={title} desc1={`"${params.command}"`} icon={<Dot className={`stroke-orange-500`} />}
// TODO!!! open the terminal with that ID
/>
},
resultWrapper: ({ toolMessage }) => {
const accessor = useAccessor()
@ -1294,22 +1311,17 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx }: ChatBubbleProps) =>
else if (role === 'tool_request') {
const isLastMessage = true // TODO!!! fix this
if (!isLastMessage) return null
const ToolMessageComponent = toolNameToComponent[chatMessage.name].requestWrapper as React.FC<{ toolRequest: any }> // ts isnt smart enough...
const ToolRequestComponent = toolNameToComponent[chatMessage.name].requestWrapper as React.FC<{ toolRequest: any }> // ts isnt smart enough...
return <>
<ToolMessageComponent
toolRequest={chatMessage}
/>
<ToolRequestComponent toolRequest={chatMessage} />
<ToolRequestAcceptRejectButtons toolRequest={chatMessage} />
</>
}
else if (role === 'tool') {
const title = toolNameToTitle[chatMessage.name]
if (chatMessage.result.type === 'error') return <ToolError title={title} errorMessage={chatMessage.result.value} />
const ToolMessageComponent = toolNameToComponent[chatMessage.name].resultWrapper as React.FC<{ toolMessage: any }> // ts isnt smart enough...
return <ToolMessageComponent
toolMessage={chatMessage}
/>
const ToolResultComponent = toolNameToComponent[chatMessage.name].resultWrapper as React.FC<{ toolMessage: any }> // ts isnt smart enough...
return <ToolResultComponent toolMessage={chatMessage} />
}
@ -1363,7 +1375,7 @@ export const SidebarChat = () => {
const initVal = ''
const [instructionsAreEmpty, setInstructionsAreEmpty] = useState(!initVal)
const isDisabled = instructionsAreEmpty || !!isFeatureNameDisabled('Ctrl+L', settingsState)
const isDisabled = instructionsAreEmpty || !!isFeatureNameDisabled('Chat', settingsState)
const [sidebarRef, sidebarDimensions] = useResizeObserver()
const [chatAreaRef, chatAreaDimensions] = useResizeObserver()
@ -1382,6 +1394,9 @@ export const SidebarChat = () => {
// send message to LLM
const userMessage = textAreaRef.current?.value ?? ''
// getModelCapabilities() // TODO!!! check if can go into agent mode
await chatThreadsService.addUserMessageAndStreamResponse({ userMessage, chatMode: 'agent' })
setSelections([]) // clear staging
@ -1420,8 +1435,9 @@ export const SidebarChat = () => {
<ChatBubble key={getChatBubbleId(currentThread.id, streamingChatIdx)}
messageIdx={streamingChatIdx} chatMessage={{
role: 'assistant',
content: messageSoFar ?? null,
reasoning: reasoningSoFar ?? null,
content: messageSoFar ?? '',
reasoning: reasoningSoFar ?? '',
anthropicReasoning: null,
}}
isLoading={isStreaming}
/> : null
@ -1492,7 +1508,6 @@ export const SidebarChat = () => {
selections={selections}
setSelections={setSelections}
onClickAnywhere={() => { textAreaRef.current?.focus() }}
featureName="Ctrl+L"
>
<VoidInputBox2
className='min-h-[81px] px-0.5'

View file

@ -21,7 +21,7 @@ import { IContextViewService, IContextMenuService } from '../../../../../../../p
import { IFileService } from '../../../../../../../platform/files/common/files.js';
import { IHoverService } from '../../../../../../../platform/hover/browser/hover.js';
import { IThemeService } from '../../../../../../../platform/theme/common/themeService.js';
import { ILLMMessageService } from '../../../../../../../workbench/contrib/void/common/llmMessageService.js';
import { ILLMMessageService } from '../../../../common/sendLLMMessageService.js';
import { IRefreshModelService } from '../../../../../../../workbench/contrib/void/common/refreshModelService.js';
import { IVoidSettingsService } from '../../../../../../../workbench/contrib/void/common/voidSettingsService.js';
import { IEditCodeService, URIStreamState } from '../../../editCodeServiceInterface.js'

View file

@ -366,15 +366,20 @@ export const AutoRefreshToggle = () => {
// right now this is just `enabled_autoRefreshModels`
const enabled = voidSettingsState.globalSettings[settingName]
return <SubtleButton
onClick={() => {
voidSettingsService.setGlobalSetting(settingName, !enabled)
metricsService.capture('Click', { action: 'Autorefresh Toggle', settingName, enabled: !enabled })
}}
text={`Automatically detect local providers and models (${refreshableProviderNames.map(providerName => displayInfoOfProviderName(providerName).title).join(', ')}).`}
icon={enabled ? <Check className='stroke-green-500 size-3' /> : <X className='stroke-red-500 size-3' />}
disabled={false}
/>
return <div className='flex items-center px-3 gap-x-1.5'>
<VoidSwitch
size='xxs'
value={enabled}
onChange={(newVal) => {
voidSettingsService.setGlobalSetting(settingName, newVal)
metricsService.capture('Click', { action: 'Autorefresh Toggle', settingName, enabled: newVal })
}} />
<span className='text-void-fg-3'>
{`Automatically detect local providers and models (${refreshableProviderNames.map(providerName => displayInfoOfProviderName(providerName).title).join(', ')}).`}
</span>
</div>
}
@ -459,7 +464,7 @@ export const FeaturesTab = () => {
<div className='w-full'>
<h4 className={`text-base`}>{displayInfoOfFeatureName('Apply')}</h4>
<div className='text-sm italic text-void-fg-3 my-1'>We recommend the smartest model you{`'`}ve got, like Claude 3.7 or GPT 4o.</div>
<div className='text-sm italic text-void-fg-3 my-1'>We recommend using Claude 3.7 or GPT 4o.</div>
<ModelDropdown featureName={'Apply'} />
</div>
</div>

View file

@ -370,7 +370,6 @@ export class ToolsService implements IToolsService {
},
edit: async (params: string) => {
console.log('validating edit!!!')
const o = validateJSON(params)
const { uri: uriStr, changeDescription: changeDescriptionUnknown } = o
const uri = validateURI(uriStr)
@ -395,7 +394,7 @@ export class ToolsService implements IToolsService {
const fromIdx = MAX_FILE_CHARS_PAGE * (pageNumber - 1)
const toIdx = MAX_FILE_CHARS_PAGE * pageNumber - 1
const fileContents = readFileContents.slice(fromIdx, toIdx + 1) || '(empty)' // paginate
const fileContents = readFileContents.slice(fromIdx, toIdx + 1) // paginate
const hasNextPage = (readFileContents.length - 1) - toIdx >= 1
return { fileContents, hasNextPage }
},
@ -446,16 +445,13 @@ export class ToolsService implements IToolsService {
},
edit: async ({ uri, changeDescription }) => {
console.log('editing!!!!')
const [_, p] = editCodeService.startApplying({
const [_, applyDonePromise] = editCodeService.startApplying({
uri,
applyStr: changeDescription,
from: 'ClickApply',
type: 'searchReplace',
}) ?? []
console.log('B')
await p
await applyDonePromise
return {}
},
terminal_command: async ({ command }) => {

View file

@ -38,7 +38,7 @@ import './voidUpdateActions.js'
// ---------- common (unclear if these actually need to be imported, because they're already imported wherever they're used) ----------
// llmMessage
import '../common/llmMessageService.js'
import '../common/sendLLMMessageService.js'
// voidSettings
import '../common/voidSettingsService.js'

View file

@ -3,7 +3,7 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { OptionsOfModelSelection, ProviderName } from './voidSettingsTypes.js';
import { ModelSelectionOptions, ProviderName } from './voidSettingsTypes.js';
export const defaultModelsOfProvider = {
@ -633,11 +633,11 @@ export const getProviderCapabilities = (providerName: ProviderName) => {
}
// state from optionsOfModelSelection
export const getModelSelectionState = (providerName: ProviderName, modelName: string, optionsOfModelSelection: OptionsOfModelSelection): { isReasoningEnabled: boolean, reasoningBudget: number | undefined } => {
const { canToggleReasoning } = getModelCapabilities(providerName, modelName).supportsReasoning || {}
export const getModelSelectionState = (providerName: ProviderName, modelName: string, modelSelectionOptions: ModelSelectionOptions | undefined): { isReasoningEnabled: boolean, reasoningBudget: number | undefined } => {
const { canToggleReasoning, reasoningBudgetSlider } = getModelCapabilities(providerName, modelName).supportsReasoning || {}
const defaultEnabledVal = canToggleReasoning ? true : false
const isReasoningEnabled = optionsOfModelSelection[providerName]?.[modelName]?.reasoningEnabled ?? defaultEnabledVal
const reasoningBudget = optionsOfModelSelection[providerName]?.[modelName]?.reasoningBudget
const isReasoningEnabled = modelSelectionOptions?.reasoningEnabled ?? defaultEnabledVal
const reasoningBudget = reasoningBudgetSlider?.type === 'slider' ? modelSelectionOptions?.reasoningBudget ?? reasoningBudgetSlider?.default : undefined
return { isReasoningEnabled, reasoningBudget }
}

View file

@ -4,11 +4,11 @@
*--------------------------------------------------------------------------------------*/
import { IVoidSettingsService } from './voidSettingsService.js';
import { ILLMMessageService } from './llmMessageService.js';
import { ILLMMessageService } from './sendLLMMessageService.js';
import { Emitter, Event } from '../../../../base/common/event.js';
import { Disposable, IDisposable } from '../../../../base/common/lifecycle.js';
import { RefreshableProviderName, refreshableProviderNames, SettingsOfProvider } from './voidSettingsTypes.js';
import { OllamaModelResponse, VLLMModelResponse } from './llmMessageTypes.js';
import { OllamaModelResponse, VLLMModelResponse } from './sendLLMMessageTypes.js';
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';

View file

@ -3,7 +3,7 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { EventLLMMessageOnTextParams, EventLLMMessageOnErrorParams, EventLLMMessageOnFinalMessageParams, ServiceSendLLMMessageParams, MainSendLLMMessageParams, MainLLMMessageAbortParams, ServiceModelListParams, EventModelListOnSuccessParams, EventModelListOnErrorParams, MainModelListParams, OllamaModelResponse, VLLMModelResponse, } from './llmMessageTypes.js';
import { EventLLMMessageOnTextParams, EventLLMMessageOnErrorParams, EventLLMMessageOnFinalMessageParams, ServiceSendLLMMessageParams, MainSendLLMMessageParams, MainLLMMessageAbortParams, ServiceModelListParams, EventModelListOnSuccessParams, EventModelListOnErrorParams, MainModelListParams, OllamaModelResponse, VLLMModelResponse, } from './sendLLMMessageTypes.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
@ -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 { displayInfoOfProviderName, isFeatureNameDisabled } from './voidSettingsTypes.js';
// import { INotificationService } from '../../notification/common/notification.js';
// calls channel to implement features
@ -67,7 +66,7 @@ export class LLMMessageService extends Disposable implements ILLMMessageService
// const service = ProxyChannel.toService<LLMMessageChannel>(mainProcessService.getChannel('void-channel-sendLLMMessage')); // lets you call it like a service
// see llmMessageChannel.ts
this.channel = this.mainProcessService.getChannel('void-channel-llmMessageService')
this.channel = this.mainProcessService.getChannel('void-channel-llmMessage')
// .listen sets up an IPC channel and takes a few ms, so we set up listeners immediately and add hooks to them instead
// llm
@ -83,32 +82,15 @@ export class LLMMessageService extends Disposable implements ILLMMessageService
}
sendLLMMessage(params: ServiceSendLLMMessageParams) {
const { onText, onFinalMessage, onError, ...proxyParams } = params;
const { useProviderFor: featureName } = proxyParams
const { onText, onFinalMessage, onError, modelSelection, ...proxyParams } = params;
// throw an error if no model/provider selected (this should usually never be reached, the UI should check this first, but might happen in cases like Apply where we haven't built much UI/checks yet, good practice to have check logic on backend)
const isDisabled = isFeatureNameDisabled(featureName, this.voidSettingsService.state)
const modelSelection = this.voidSettingsService.state.modelSelectionOfFeature[featureName]
if (isDisabled || modelSelection === null) {
let message: string
if (isDisabled === 'addProvider' || isDisabled === 'providerNotAutoDetected')
message = `Please add a provider in Void's Settings.`
else if (isDisabled === 'addModel')
message = `Please add a model.`
else if (isDisabled === 'needToEnableModel')
message = `Please enable a model.`
else if (isDisabled === 'notFilledIn')
message = `Please fill in Void's Settings${modelSelection !== null ? ` for ${displayInfoOfProviderName(modelSelection.providerName).title}` : ''}.`
else
message = `Please add a provider in Void's Settings.`
if (modelSelection === null) {
const message = `Please add a provider in Void's Settings.`
onError({ message, fullError: null })
return null
}
const { providerName, modelName } = modelSelection
// add state for request id
const requestId = generateUuid();
this.llmMessageHooks.onText[requestId] = onText
@ -116,17 +98,15 @@ export class LLMMessageService extends Disposable implements ILLMMessageService
this.llmMessageHooks.onError[requestId] = onError
const { aiInstructions } = this.voidSettingsService.state.globalSettings
const { settingsOfProvider, optionsOfModelSelection, } = this.voidSettingsService.state
const { settingsOfProvider, } = this.voidSettingsService.state
// params will be stripped of all its functions over the IPC channel
this.channel.call('sendLLMMessage', {
...proxyParams,
aiInstructions,
requestId,
providerName,
modelName,
settingsOfProvider,
optionsOfModelSelection,
modelSelection,
} satisfies MainSendLLMMessageParams);
return requestId

View file

@ -3,9 +3,8 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import type { ChatMessage } from '../browser/chatThreadService.js'
import type { InternalToolInfo, ToolName } from '../browser/toolsService.js'
import { FeatureName, OptionsOfModelSelection, ProviderName, SettingsOfProvider } from './voidSettingsTypes.js'
import { ModelSelection, ModelSelectionOptions, ProviderName, SettingsOfProvider } from './voidSettingsTypes.js'
export const errorDetails = (fullError: Error | null): string | null => {
@ -29,12 +28,15 @@ export const getErrorMessage: (error: unknown) => string = (error) => {
export type LLMChatMessage = {
role: 'system' | 'user';
role: 'system';
content: string;
} | {
role: 'user';
content: string;
} | {
role: 'assistant',
content: string; // text content
rawAnthropicAssistantContent?: RawAnthropicAssistantContent[]; // used for anthropic signing
anthropicReasoning: AnthropicReasoning[] | null;
} | {
role: 'tool';
content: string; // result
@ -50,31 +52,14 @@ export type ToolCallType = {
id: string;
}
export type RawAnthropicAssistantContent = { type: 'thinking'; thinking: string; signature: string; } | { type: 'redacted_thinking'; data: string } | { type: 'text', text: string }
export type AnthropicReasoning = ({ type: 'thinking'; thinking: any; signature: string; } | { type: 'redacted_thinking', data: any })
export type OnText = (p: { fullText: string; fullReasoning: string }) => void
export type OnFinalMessage = (p: { fullText: string, toolCalls?: ToolCallType[], fullReasoning?: string, rawAnthropicAssistantContent?: RawAnthropicAssistantContent[] }) => void // id is tool_use_id
export type OnError = (p: { message: string, fullError: Error | null }) => void
export type OnFinalMessage = (p: { fullText: string; fullReasoning: string; toolCalls?: ToolCallType[]; anthropicReasoning: AnthropicReasoning[] | null }) => void // id is tool_use_id
export type OnError = (p: { message: string; fullError: Error | null }) => void
export type AbortRef = { current: (() => void) | null }
export const toLLMChatMessage = (c: ChatMessage): LLMChatMessage | null => {
if (c.role === 'user') {
return { role: c.role, content: c.content || '(empty message)' }
}
else if (c.role === 'assistant')
return { role: c.role, content: c.content || '(empty message)' }
else if (c.role === 'tool')
return { role: c.role, id: c.id, name: c.name, params: c.paramsStr, content: c.content || '(empty output)' }
else if (c.role === 'tool_request')
return null
else {
throw new Error(`Role ${(c as any).role} not recognized.`)
}
}
export type LLMFIMMessage = {
prefix: string;
suffix: string;
@ -97,8 +82,9 @@ export type ServiceSendLLMMessageParams = {
onFinalMessage: OnFinalMessage;
onError: OnError;
logging: { loggingName: string, };
useProviderFor: FeatureName;
} & SendLLMType
modelSelection: ModelSelection | null;
modelSelectionOptions: ModelSelectionOptions | undefined;
} & SendLLMType;
// params to the true sendLLMMessage function
export type SendLLMMessageParams = {
@ -110,10 +96,10 @@ export type SendLLMMessageParams = {
aiInstructions: string;
providerName: ProviderName;
modelName: string;
modelSelection: ModelSelection;
modelSelectionOptions: ModelSelectionOptions | undefined;
settingsOfProvider: SettingsOfProvider;
optionsOfModelSelection: OptionsOfModelSelection;
} & SendLLMType

View file

@ -14,8 +14,10 @@ import { IMetricsService } from './metricsService.js';
import { getModelCapabilities } from './modelCapabilities.js';
import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, VoidModelInfo, GlobalSettings, GlobalSettingName, defaultGlobalSettings, defaultProviderSettings, ModelSelectionOptions, OptionsOfModelSelection } from './voidSettingsTypes.js';
// past values:
// 'void.settingsServiceStorage'
const STORAGE_KEY = 'void.settingsServiceStorage'
const STORAGE_KEY = 'void.settingsServiceStorageI'
// name is the name in the dropdown
@ -97,7 +99,7 @@ const _updatedModelsAfterDefaultModelsChange = (defaultModelNames: string[], opt
export const modelFilterOfFeatureName: { [featureName in FeatureName]: { filter: (o: ModelSelection) => boolean; emptyMessage: string | null } } = {
'Autocomplete': { filter: o => getModelCapabilities(o.providerName, o.modelName).supportsFIM, emptyMessage: 'No models support FIM' },
'Ctrl+L': { filter: o => true, emptyMessage: null },
'Chat': { filter: o => true, emptyMessage: null },
'Ctrl+K': { filter: o => true, emptyMessage: null },
'Apply': { filter: o => true, emptyMessage: null },
}
@ -172,7 +174,7 @@ const _validatedState = (state: Omit<VoidSettingsState, '_modelOptions'>) => {
const defaultState = () => {
const d: VoidSettingsState = {
settingsOfProvider: deepClone(defaultSettingsOfProvider),
modelSelectionOfFeature: { 'Ctrl+L': null, 'Ctrl+K': null, 'Autocomplete': null, 'Apply': null },
modelSelectionOfFeature: { 'Chat': null, 'Ctrl+K': null, 'Autocomplete': null, 'Apply': null },
globalSettings: deepClone(defaultGlobalSettings),
optionsOfModelSelection: {},
_modelOptions: [], // computed later

View file

@ -310,7 +310,7 @@ export const modelSelectionsEqual = (m1: ModelSelection, m2: ModelSelection) =>
}
// this is a state
export const featureNames = ['Ctrl+L', 'Ctrl+K', 'Autocomplete', 'Apply'] as const
export const featureNames = ['Chat', 'Ctrl+K', 'Autocomplete', 'Apply'] as const
export type ModelSelectionOfFeature = Record<(typeof featureNames)[number], ModelSelection | null>
export type FeatureName = keyof ModelSelectionOfFeature
@ -321,7 +321,7 @@ export const displayInfoOfFeatureName = (featureName: FeatureName) => {
else if (featureName === 'Ctrl+K')
return 'Quick Edit'
// sidebar:
else if (featureName === 'Ctrl+L')
else if (featureName === 'Chat')
return 'Chat'
else if (featureName === 'Apply')
return 'Fast Apply'

View file

@ -3,7 +3,7 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { RawAnthropicAssistantContent, LLMChatMessage, LLMFIMMessage } from '../../common/llmMessageTypes.js';
import { AnthropicReasoning, LLMChatMessage, LLMFIMMessage } from '../../common/sendLLMMessageTypes.js';
import { deepClone } from '../../../../../base/common/objects.js';
@ -22,8 +22,7 @@ type InternalLLMChatMessage = {
content: string;
} | {
role: 'assistant',
content: string | (RawAnthropicAssistantContent | { type: 'text'; text: string })[];
rawAnthropicAssistantContent?: RawAnthropicAssistantContent[] | undefined;
content: string | (AnthropicReasoning | { type: 'text'; text: string })[];
} | {
role: 'tool';
content: string; // result
@ -33,7 +32,10 @@ type InternalLLMChatMessage = {
}
const prepareMessages_normalize = ({ messages: messages_ }: { messages: LLMChatMessage[] }) => {
const EMPTY_MESSAGE = '(empty message)'
const EMPTY_TOOL_CONTENT = '(empty content)'
const prepareMessages_normalize = ({ messages: messages_ }: { messages: LLMChatMessage[] }): { messages: LLMChatMessage[] } => {
const messages = deepClone(messages_)
const newMessages: LLMChatMessage[] = []
if (messages.length >= 0) newMessages.push(messages[0])
@ -41,12 +43,12 @@ const prepareMessages_normalize = ({ messages: messages_ }: { messages: LLMChatM
// remove duplicate roles
for (let i = 1; i < messages.length; i += 1) {
const curr = messages[i]
const prev = messages[i - 1]
// if found a repeated role, put the current content in the prev
if ((curr.role === 'user' && prev.role === 'user') || (curr.role === 'assistant' && prev.role === 'assistant')) {
prev.content += '\n' + curr.content
continue
}
// const prev = messages[i - 1]
// // if found a repeated role, put the current content in the prev
// if ((curr.role === 'assistant' && prev.role === 'assistant')) {
// prev.content += '\n' + curr.content
// continue
// }
// add the message
newMessages.push(curr)
}
@ -58,29 +60,6 @@ const prepareMessages_normalize = ({ messages: messages_ }: { messages: LLMChatM
// remove rawAnthropicAssistantContent, and make content equal to it if supportsAnthropicContent
const prepareMessages_anthropicContent = ({ messages, supportsAnthropicContent }: { messages: LLMChatMessage[], supportsAnthropicContent: boolean }) => {
const newMessages: InternalLLMChatMessage[] = []
for (const m of messages) {
if (m.role !== 'assistant') {
newMessages.push(m)
continue
}
let newMessage: InternalLLMChatMessage
if (supportsAnthropicContent) {
const newContent = m.rawAnthropicAssistantContent
newMessage = { role: 'assistant', content: newContent ?? m.content }
}
else {
newMessage = m
}
delete newMessage.rawAnthropicAssistantContent // important to delete this field
newMessages.push(m)
}
return { messages: newMessages }
}
// no matter whether the model supports a system message or not (or what format it supports), add it in some way
@ -173,27 +152,27 @@ openai on prompting - https://platform.openai.com/docs/guides/reasoning#advice-o
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: (
Exclude<InternalLLMChatMessage, { role: 'assistant' | 'tool' }> | {
role: 'assistant',
content: string | object[];
tool_calls?: {
type: 'function';
id: string;
function: {
name: string;
arguments: string;
}
}[]
} | {
role: 'tool',
id: string; // old val
tool_call_id: string; // new val
content: string;
}
)[] = [];
const newMessages: PrepareMessagesToolsOpenAI = [];
for (let i = 0; i < messages.length; i += 1) {
const currMsg = messages[i]
@ -219,9 +198,8 @@ const prepareMessages_tools_openai = ({ messages }: { messages: InternalLLMChatM
// add the tool
newMessages.push({
role: 'tool',
id: currMsg.id,
content: currMsg.content,
tool_call_id: currMsg.id,
content: currMsg.content || EMPTY_TOOL_CONTENT,
})
}
return { messages: newMessages }
@ -250,34 +228,44 @@ anthropic RESPONSE (role=user):
}]
*/
const prepareMessages_tools_anthropic = ({ messages }: { messages: InternalLLMChatMessage[], }) => {
const newMessages: (
Exclude<InternalLLMChatMessage, { role: 'assistant' | 'user' }> | {
role: 'assistant',
content: string | (
| RawAnthropicAssistantContent
| {
type: 'text';
text: string;
}
| {
type: 'tool_use';
name: string;
input: Record<string, any>;
id: string;
})[]
} | {
role: 'user',
content: string | ({
type PrepareMessagesToolsAnthropic = (
Exclude<InternalLLMChatMessage, { role: 'assistant' | 'user' }> | {
role: 'assistant',
content: string | (
| AnthropicReasoning
| {
type: 'text';
text: string;
} | {
type: 'tool_result';
tool_use_id: string;
content: string;
}
| {
type: 'tool_use';
name: string;
input: Record<string, any>;
id: string;
})[]
}
)[] = messages;
} | {
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) {
@ -296,7 +284,7 @@ const prepareMessages_tools_anthropic = ({ messages }: { messages: InternalLLMCh
newMessages[i] = {
role: 'user',
content: [
...[{ type: 'tool_result', tool_use_id: currMsg.id, content: currMsg.content }] as const,
...[{ type: 'tool_result', tool_use_id: currMsg.id, content: currMsg.content || EMPTY_TOOL_CONTENT }] as const,
...currMsg.content ? [{ type: 'text', text: currMsg.content }] as const : [],
]
}
@ -307,8 +295,9 @@ const prepareMessages_tools_anthropic = ({ messages }: { messages: InternalLLMCh
type PrepareMessagesTools = PrepareMessagesToolsAnthropic | PrepareMessagesToolsOpenAI
const prepareMessages_tools = ({ messages, supportsTools }: { messages: InternalLLMChatMessage[], supportsTools: false | 'anthropic-style' | 'openai-style' }) => {
const prepareMessages_tools = ({ messages, supportsTools }: { messages: InternalLLMChatMessage[], supportsTools: false | 'anthropic-style' | 'openai-style' }): { messages: PrepareMessagesTools } => {
if (!supportsTools) {
return { messages: messages }
}
@ -324,6 +313,128 @@ const prepareMessages_tools = ({ messages, supportsTools }: { messages: Internal
}
// remove rawAnthropicAssistantContent, and make content equal to it if supportsAnthropicContent
const prepareMessages_anthropicContent = ({ messages, supportsAnthropicReasoningSignature }: { messages: LLMChatMessage[], supportsAnthropicReasoningSignature: boolean }) => {
const newMessages: InternalLLMChatMessage[] = []
for (const m of messages) {
if (m.role !== 'assistant') {
newMessages.push(m)
continue
}
let newMessage: InternalLLMChatMessage
if (supportsAnthropicReasoningSignature && m.anthropicReasoning) {
const content = m.content ? [...m.anthropicReasoning, { type: 'text' as const, text: m.content }] : m.anthropicReasoning
newMessage = { role: 'assistant', content: content }
}
else {
newMessage = { role: 'assistant', content: m.content }
}
newMessages.push(newMessage)
}
return { messages: newMessages }
}
// do this at end
const prepareMessages_noEmptyMessage = ({ messages }: { messages: PrepareMessagesTools }): { messages: PrepareMessagesTools } => {
for (const currMsg of messages) {
// don't do this for tools
if (currMsg.role === 'tool') continue
// don't do this for assistant or user messages that have tool_calls or tool_results
const oai = currMsg as PrepareMessagesToolsOpenAI[0]
if (oai.role === 'assistant') {
if (oai.tool_calls) continue
}
const anth = currMsg as PrepareMessagesToolsAnthropic[0]
if (anth.role === 'assistant' || anth.role === 'user') {
if (typeof anth.content !== 'string') {
const hasContent = anth.content.find(c => c.type === 'tool_use' || c.type === 'tool_result')
if (hasContent) continue
}
}
if (typeof currMsg.content === 'string') {
currMsg.content = currMsg.content || EMPTY_MESSAGE
}
else {
for (const c of currMsg.content) {
if (c.type === 'text') c.text = c.text || EMPTY_MESSAGE
else if (c.type === 'tool_use') { }
else if (c.type === 'tool_result') { }
}
}
}
return { messages }
}
// --- CHAT ---
export const prepareMessages = ({
messages,
aiInstructions,
supportsSystemMessage,
supportsTools,
supportsAnthropicReasoningSignature,
}: {
messages: LLMChatMessage[],
aiInstructions: string,
supportsSystemMessage: false | 'system-role' | 'developer-role' | 'separated',
supportsTools: false | 'anthropic-style' | 'openai-style',
supportsAnthropicReasoningSignature: boolean,
}) => {
const { messages: messages1 } = prepareMessages_normalize({ messages })
const { messages: messages2 } = prepareMessages_anthropicContent({ messages: messages1, supportsAnthropicReasoningSignature })
const { messages: messages3, separateSystemMessageStr } = prepareMessages_systemMessage({ messages: messages2, aiInstructions, supportsSystemMessage })
const { messages: messages4 } = prepareMessages_tools({ messages: messages3, supportsTools })
const { messages: messages5 } = prepareMessages_noEmptyMessage({ messages: messages4 })
return {
messages: messages5 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
}
@ -357,60 +468,3 @@ gemini response:
// --- CHAT ---
export const prepareMessages = ({
messages,
aiInstructions,
supportsSystemMessage,
supportsTools,
supportsAnthropicContent,
}: {
messages: LLMChatMessage[],
aiInstructions: string,
supportsSystemMessage: false | 'system-role' | 'developer-role' | 'separated',
supportsTools: false | 'anthropic-style' | 'openai-style',
supportsAnthropicContent: boolean,
}) => {
const { messages: messages1 } = prepareMessages_normalize({ messages })
const { messages: messages2 } = prepareMessages_anthropicContent({ messages: messages1, supportsAnthropicContent })
const { messages: messages3, separateSystemMessageStr } = prepareMessages_systemMessage({ messages: messages2, aiInstructions, supportsSystemMessage })
const { messages: messages4 } = prepareMessages_tools({ messages: messages3, supportsTools })
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
}

View file

@ -9,9 +9,9 @@ import OpenAI, { ClientOptions } from 'openai';
import { Model as OpenAIModel } from 'openai/resources/models.js';
import { extractReasoningOnFinalMessage, extractReasoningOnTextWrapper } from '../../browser/helpers/extractCodeFromResult.js';
import { LLMChatMessage, LLMFIMMessage, ModelListParams, OllamaModelResponse, OnError, OnFinalMessage, OnText } from '../../common/llmMessageTypes.js';
import { LLMChatMessage, LLMFIMMessage, ModelListParams, OllamaModelResponse, OnError, OnFinalMessage, OnText } from '../../common/sendLLMMessageTypes.js';
import { InternalToolInfo, isAToolName, ToolName } from '../../browser/toolsService.js';
import { defaultProviderSettings, displayInfoOfProviderName, OptionsOfModelSelection, ProviderName, SettingsOfProvider } from '../../common/voidSettingsTypes.js';
import { defaultProviderSettings, displayInfoOfProviderName, ModelSelectionOptions, ProviderName, SettingsOfProvider } from '../../common/voidSettingsTypes.js';
import { prepareFIMMessage, prepareMessages } from './preprocessLLMMessages.js';
import { getModelSelectionState, getModelCapabilities, getProviderCapabilities } from '../../common/modelCapabilities.js';
@ -23,7 +23,7 @@ type InternalCommonMessageParams = {
onError: OnError;
providerName: ProviderName;
settingsOfProvider: SettingsOfProvider;
optionsOfModelSelection: OptionsOfModelSelection;
modelSelectionOptions: ModelSelectionOptions | undefined;
modelName: string;
_setAborter: (aborter: () => void) => void;
}
@ -140,7 +140,7 @@ const _sendOpenAICompatibleFIM = ({ messages: messages_, onFinalMessage, onError
})
.then(async response => {
const fullText = response.choices[0]?.text
onFinalMessage({ fullText, });
onFinalMessage({ fullText, fullReasoning: '', anthropicReasoning: null });
})
.catch(error => {
if (error instanceof OpenAI.APIError && error.status === 401) { onError({ message: invalidApiKeyMessage(providerName), fullError: error }); }
@ -168,7 +168,7 @@ const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage
const { providerReasoningIOSettings } = getProviderCapabilities(providerName)
const { messages } = prepareMessages({ messages: messages_, aiInstructions, supportsSystemMessage, supportsTools, supportsAnthropicContent: false }) // can change supportsAnthropicContent if e.g. OpenRouter starts supporting anthropic extended thinking
const { messages } = prepareMessages({ messages: messages_, aiInstructions, supportsSystemMessage, supportsTools, supportsAnthropicReasoningSignature: false })
const tools = (supportsTools && ((tools_?.length ?? 0) !== 0)) ? tools_?.map(tool => toOpenAICompatibleTool(tool)) : undefined
const includeInPayload = canIOReasoning ? providerReasoningIOSettings?.input?.includeInPayload || {} : {}
@ -222,9 +222,9 @@ const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage
else {
if (manuallyParseReasoning) {
const { fullText, fullReasoning } = extractReasoningOnFinalMessage(fullTextSoFar, openSourceThinkTags)
onFinalMessage({ fullText, fullReasoning, toolCalls });
onFinalMessage({ fullText, fullReasoning, toolCalls, anthropicReasoning: null });
} else {
onFinalMessage({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar, toolCalls });
onFinalMessage({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar, toolCalls, anthropicReasoning: null });
}
}
})
@ -280,7 +280,7 @@ const toAnthropicTool = (toolInfo: InternalToolInfo) => {
} satisfies Anthropic.Messages.Tool
}
const toolCallsFrom_AnthropicContent = (content: Anthropic.Messages.ContentBlock[]): ToolCallsFrom_ReturnType => {
const toolCallsFrom_Anthropic = (content: Anthropic.Messages.ContentBlock[]): ToolCallsFrom_ReturnType => {
return content.map(c => {
if (c.type !== 'tool_use') return null
if (!isAToolName(c.name)) return null
@ -288,7 +288,7 @@ const toolCallsFrom_AnthropicContent = (content: Anthropic.Messages.ContentBlock
}).filter(t => !!t)
}
const sendAnthropicChat = ({ messages: messages_, providerName, onText, onFinalMessage, onError, settingsOfProvider, optionsOfModelSelection, modelName: modelName_, _setAborter, aiInstructions, tools: tools_ }: SendChatParams_Internal) => {
const sendAnthropicChat = ({ messages: messages_, providerName, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName: modelName_, _setAborter, aiInstructions, tools: tools_ }: SendChatParams_Internal) => {
const {
modelName,
supportsSystemMessage,
@ -299,9 +299,9 @@ const sendAnthropicChat = ({ messages: messages_, providerName, onText, onFinalM
const {
isReasoningEnabled,
reasoningBudget,
} = getModelSelectionState(providerName, modelName_, optionsOfModelSelection) // user's modelName_ here
} = getModelSelectionState(providerName, modelName_, modelSelectionOptions) // user's modelName_ here
const { messages, separateSystemMessageStr } = prepareMessages({ messages: messages_, aiInstructions, supportsSystemMessage, supportsTools, supportsAnthropicContent: true })
const { messages, separateSystemMessageStr } = prepareMessages({ messages: messages_, aiInstructions, supportsSystemMessage, supportsTools, supportsAnthropicReasoningSignature: true })
const thisConfig = settingsOfProvider.anthropic
const anthropic = new Anthropic({ apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true });
@ -370,8 +370,9 @@ const sendAnthropicChat = ({ messages: messages_, providerName, onText, onFinalM
// on done - (or when error/fail) - this is called AFTER last streamEvent
stream.on('finalMessage', (response) => {
const toolCalls = toolCallsFrom_AnthropicContent(response.content)
onFinalMessage({ fullText, fullReasoning, toolCalls, rawAnthropicAssistantContent: response.content as any })
const toolCalls = toolCallsFrom_Anthropic(response.content)
const anthropicReasoning = response.content.filter(c => c.type === 'thinking' || c.type === 'redacted_thinking')
onFinalMessage({ fullText, fullReasoning, toolCalls, anthropicReasoning })
})
// on error
stream.on('error', (error) => {
@ -455,7 +456,7 @@ const sendOllamaFIM = ({ messages: messages_, onFinalMessage, onError, settingsO
const newText = chunk.response
fullText += newText
}
onFinalMessage({ fullText })
onFinalMessage({ fullText, fullReasoning: '', anthropicReasoning: null })
})
// when error/fail
.catch((error) => {
@ -494,6 +495,11 @@ export const sendLLMMessageToProviderImplementation = {
sendFIM: null,
list: null,
},
// mistral: {
// sendChat: , // TODO
// sendFIM: , // TODO // https://docs.mistral.ai/api/#tag/fim
// list: null,
// },
ollama: {
sendChat: (params) => _sendOpenAICompatibleChat(params),
sendFIM: sendOllamaFIM,

View file

@ -3,7 +3,7 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { SendLLMMessageParams, OnText, OnFinalMessage, OnError } from '../../common/llmMessageTypes.js';
import { SendLLMMessageParams, OnText, OnFinalMessage, OnError } from '../../common/sendLLMMessageTypes.js';
import { IMetricsService } from '../../common/metricsService.js';
import { displayInfoOfProviderName } from '../../common/voidSettingsTypes.js';
import { sendLLMMessageToProviderImplementation } from './sendLLMMessage.impl.js';
@ -19,9 +19,8 @@ export const sendLLMMessage = ({
abortRef: abortRef_,
logging: { loggingName },
settingsOfProvider,
optionsOfModelSelection,
providerName,
modelName,
modelSelection,
modelSelectionOptions,
tools,
}: SendLLMMessageParams,
@ -29,6 +28,8 @@ export const sendLLMMessage = ({
) => {
const { providerName, modelName } = modelSelection
// only captures number of messages and message "shape", no actual code, instructions, prompts, etc
const captureLLMEvent = (eventId: string, extras?: object) => {
metricsService.capture(eventId, {
@ -105,18 +106,19 @@ export const sendLLMMessage = ({
}
const { sendFIM, sendChat } = implementation
if (messagesType === 'chatMessages') {
sendChat({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, optionsOfModelSelection, modelName, _setAborter, providerName, aiInstructions, tools })
sendChat({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName, _setAborter, providerName, aiInstructions, tools })
return
}
if (messagesType === 'FIMMessage') {
if (sendFIM) {
sendFIM({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, optionsOfModelSelection, modelName, _setAborter, providerName, aiInstructions })
sendFIM({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName, _setAborter, providerName, aiInstructions })
return
}
onError({ message: `Error: This provider does not support Autocomplete yet.`, fullError: null })
return
}
onError({ message: `Error: Message type "${messagesType}" not recognized.`, fullError: null })
return
}
catch (error) {

View file

@ -8,7 +8,7 @@
import { IServerChannel } from '../../../../base/parts/ipc/common/ipc.js';
import { Emitter, Event } from '../../../../base/common/event.js';
import { EventLLMMessageOnTextParams, EventLLMMessageOnErrorParams, EventLLMMessageOnFinalMessageParams, MainSendLLMMessageParams, AbortRef, SendLLMMessageParams, MainLLMMessageAbortParams, ModelListParams, EventModelListOnSuccessParams, EventModelListOnErrorParams, OllamaModelResponse, VLLMModelResponse, MainModelListParams, } from '../common/llmMessageTypes.js';
import { EventLLMMessageOnTextParams, EventLLMMessageOnErrorParams, EventLLMMessageOnFinalMessageParams, MainSendLLMMessageParams, AbortRef, SendLLMMessageParams, MainLLMMessageAbortParams, ModelListParams, EventModelListOnSuccessParams, EventModelListOnErrorParams, OllamaModelResponse, VLLMModelResponse, MainModelListParams, } from '../common/sendLLMMessageTypes.js';
import { sendLLMMessage } from './llmMessage/sendLLMMessage.js'
import { IMetricsService } from '../common/metricsService.js';
import { sendLLMMessageToProviderImplementation } from './llmMessage/sendLLMMessage.impl.js';