mirror of
https://github.com/voideditor/void
synced 2026-05-23 09:28:23 +00:00
Merge pull request #400 from voideditor/model-selection
GPT 4.1 and Claude 3.7 Thinking with Tools
This commit is contained in:
commit
eda8517c78
21 changed files with 1072 additions and 921 deletions
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"nameShort": "Void",
|
||||
"nameLong": "Void",
|
||||
"voidVersion": "1.2.0",
|
||||
"voidVersion": "1.2.1",
|
||||
"applicationName": "void",
|
||||
"dataFolderName": ".void-editor",
|
||||
"win32MutexName": "voideditor",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
|
||||
|
||||
|
|
@ -47,6 +47,7 @@ import { IVoidModelService } from '../common/voidModelService.js';
|
|||
import { deepClone } from '../../../../base/common/objects.js';
|
||||
import { acceptBg, acceptBorder, buttonFontSize, buttonTextColor, rejectBg, rejectBorder } from '../common/helpers/colors.js';
|
||||
import { DiffArea, Diff, CtrlKZone, VoidFileSnapshot, DiffAreaSnapshotEntry, diffAreaSnapshotKeys, DiffZone, TrackingZone, ComputedDiff } from '../common/editCodeServiceTypes.js';
|
||||
import { IConvertToLLMMessageService } from './convertToLLMMessageService.js';
|
||||
|
||||
const configOfBG = (color: Color) => {
|
||||
return { dark: color, light: color, hcDark: color, hcLight: color, }
|
||||
|
|
@ -202,6 +203,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
@IVoidSettingsService private readonly _settingsService: IVoidSettingsService,
|
||||
// @IFileService private readonly _fileService: IFileService,
|
||||
@IVoidModelService private readonly _voidModelService: IVoidModelService,
|
||||
@IConvertToLLMMessageService private readonly _convertToLLMMessageService: IConvertToLLMMessageService,
|
||||
) {
|
||||
super();
|
||||
|
||||
|
|
@ -1267,6 +1269,10 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
private _initializeWriteoverStream(opts: StartApplyingOpts): [DiffZone, Promise<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 +1306,16 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
const originalCode = startRange === 'fullFile' ? originalFileCode : originalFileCode.split('\n').slice((startRange[0] - 1), (startRange[1] - 1) + 1).join('\n')
|
||||
const language = model.getLanguageId()
|
||||
let messages: LLMChatMessage[]
|
||||
let separateSystemMessage: string | undefined
|
||||
if (from === 'ClickApply') {
|
||||
const userContent = rewriteCode_userMessage({ originalCode, applyStr: opts.applyStr, language })
|
||||
messages = [
|
||||
{ role: 'system', content: rewriteCode_systemMessage, },
|
||||
{ role: 'user', content: userContent, }
|
||||
]
|
||||
const { messages: a, separateSystemMessage: b } = this._convertToLLMMessageService.prepareLLMSimpleMessages({
|
||||
systemMessage: rewriteCode_systemMessage,
|
||||
simpleMessages: [{ role: 'user', content: rewriteCode_userMessage({ originalCode, applyStr: opts.applyStr, language }), }],
|
||||
featureName,
|
||||
modelSelection,
|
||||
})
|
||||
messages = a
|
||||
separateSystemMessage = b
|
||||
}
|
||||
else if (from === 'QuickEdit') {
|
||||
if (!ctrlKZoneIfQuickEdit) return
|
||||
|
|
@ -1316,11 +1326,16 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
const endLine = startRange === 'fullFile' ? model.getLineCount() : startRange[1]
|
||||
const { prefix, suffix } = voidPrefixAndSuffix({ fullFileStr: originalFileCode, startLine, endLine })
|
||||
const userContent = ctrlKStream_userMessage({ selection: originalCode, instructions: instructions, prefix, suffix, fimTags: quickEditFIMTags, language })
|
||||
// type: 'messages',
|
||||
messages = [
|
||||
{ role: 'system', content: ctrlKStream_systemMessage({ quickEditFIMTags: quickEditFIMTags }), },
|
||||
{ role: 'user', content: userContent, }
|
||||
]
|
||||
|
||||
const { messages: a, separateSystemMessage: b } = this._convertToLLMMessageService.prepareLLMSimpleMessages({
|
||||
systemMessage: ctrlKStream_systemMessage({ quickEditFIMTags: quickEditFIMTags }),
|
||||
simpleMessages: [{ role: 'user', content: userContent, }],
|
||||
featureName,
|
||||
modelSelection,
|
||||
})
|
||||
messages = a
|
||||
separateSystemMessage = b
|
||||
|
||||
}
|
||||
else { throw new Error(`featureName ${from} is invalid`) }
|
||||
|
||||
|
|
@ -1384,10 +1399,6 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
|
||||
const latestStreamLocationMutable: StreamLocationMutable = { line: diffZone.startLine, addedSplitYet: false, col: 1, originalCodeStartLine: 1 }
|
||||
|
||||
const featureName: FeatureName = opts.from === 'ClickApply' ? 'Apply' : 'Ctrl+K'
|
||||
const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName]
|
||||
const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[featureName][modelSelection.providerName]?.[modelSelection.modelName] : undefined
|
||||
|
||||
// allowed to throw errors - this is called inside a promise that handles everything
|
||||
const runWriteover = async () => {
|
||||
let shouldSendAnotherMessage = true
|
||||
|
|
@ -1410,6 +1421,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
messages,
|
||||
modelSelection,
|
||||
modelSelectionOptions,
|
||||
separateSystemMessage,
|
||||
chatMode: null, // not chat
|
||||
onText: (params) => {
|
||||
const { fullText: fullText_ } = params
|
||||
|
|
@ -1485,6 +1497,9 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
|
||||
private _initializeSearchAndReplaceStream(opts: StartApplyingOpts & { from: 'ClickApply' }): [DiffZone, Promise<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 +1513,13 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
// build messages - ask LLM to generate search/replace block text
|
||||
const originalFileCode = model.getValue(EndOfLinePreference.LF)
|
||||
const userMessageContent = searchReplace_userMessage({ originalCode: originalFileCode, applyStr: applyStr })
|
||||
const messages: LLMChatMessage[] = [
|
||||
{ role: 'system', content: searchReplace_systemMessage },
|
||||
{ role: 'user', content: userMessageContent },
|
||||
]
|
||||
|
||||
const { messages, separateSystemMessage: separateSystemMessage } = this._convertToLLMMessageService.prepareLLMSimpleMessages({
|
||||
systemMessage: searchReplace_systemMessage,
|
||||
simpleMessages: [{ role: 'user', content: userMessageContent, }],
|
||||
featureName,
|
||||
modelSelection,
|
||||
})
|
||||
|
||||
// if URI is already streaming, return (should never happen, caller is responsible for checking)
|
||||
if (this._uriIsStreaming(uri)) return
|
||||
|
|
@ -1593,10 +1611,6 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
const addedTrackingZoneOfBlockNum: TrackingZone<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 +1642,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
messages,
|
||||
modelSelection,
|
||||
modelSelectionOptions,
|
||||
separateSystemMessage,
|
||||
chatMode: null, // not chat
|
||||
onText: (params) => {
|
||||
const { fullText } = params
|
||||
|
|
@ -1682,7 +1697,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
console.log('---------')
|
||||
const content = errContentOfInvalidStr(originalBounds, block.orig, blockNum, blocks)
|
||||
messages.push(
|
||||
{ role: 'assistant', content: fullText, anthropicReasoning: null }, // latest output
|
||||
{ role: 'assistant', content: fullText }, // latest output
|
||||
{ role: 'user', content: content } // user explanation of what's wrong
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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,15 +304,15 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
|
|||
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) }}
|
||||
|
|
@ -325,7 +325,7 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
|
|||
)
|
||||
}
|
||||
|
||||
if (t.type === "image") {
|
||||
if (t.type === 'image') {
|
||||
return <img
|
||||
src={t.href}
|
||||
alt={t.text}
|
||||
|
|
@ -334,16 +334,16 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
|
|||
/>
|
||||
}
|
||||
|
||||
if (t.type === "strong") {
|
||||
if (t.type === 'strong') {
|
||||
return <strong>{t.text}</strong>
|
||||
}
|
||||
|
||||
if (t.type === "em") {
|
||||
if (t.type === 'em') {
|
||||
return <em>{t.text}</em>
|
||||
}
|
||||
|
||||
// inline code
|
||||
if (t.type === "codespan" || t.type === "html") {
|
||||
if (t.type === 'codespan') {
|
||||
|
||||
if (options.isLinkDetectionEnabled && chatMessageLocation) {
|
||||
return <CodespanWithLink
|
||||
|
|
@ -357,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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ import { acceptAllBg, acceptBorder, buttonFontSize, buttonTextColor, rejectAllBg
|
|||
import { ToolName, toolNames } from '../../../../common/prompt/prompts.js';
|
||||
import { error } from 'console';
|
||||
import { RawToolCallObj } from '../../../../common/sendLLMMessageTypes.js';
|
||||
import { MAX_FILE_CHARS_PAGE } from '../../../toolsService.js';
|
||||
|
||||
|
||||
|
||||
|
|
@ -1298,7 +1299,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}
|
||||
|
|
@ -1434,17 +1435,17 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
|
|||
const componentParams: ToolHeaderParams = { title, desc1, isError, icon }
|
||||
|
||||
if (toolMessage.params.startLine !== null || toolMessage.params.endLine !== null) {
|
||||
const start = toolMessage.params.startLine === null ? `start` : `${toolMessage.params.startLine}`
|
||||
const end = toolMessage.params.endLine === null ? `end` : `${toolMessage.params.endLine}`
|
||||
const start = toolMessage.params.startLine === null ? `1` : `${toolMessage.params.startLine}`
|
||||
const end = toolMessage.params.endLine === null ? `` : `${toolMessage.params.endLine}`
|
||||
const addStr = `(${start}-${end})`
|
||||
componentParams.title += ` ${addStr}`
|
||||
componentParams.desc1 += ` ${addStr}`
|
||||
}
|
||||
|
||||
if (toolMessage.type === 'success') {
|
||||
const { params, result } = toolMessage
|
||||
componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }
|
||||
if (result.hasNextPage && params.pageNumber === 1) // first page
|
||||
componentParams.desc2 = '(more content available)'
|
||||
componentParams.desc2 = `(first ${Math.round(MAX_FILE_CHARS_PAGE) / 1000}k)`
|
||||
else if (params.pageNumber > 1) // subsequent pages
|
||||
componentParams.desc2 = `(part ${params.pageNumber})`
|
||||
}
|
||||
|
|
@ -2492,7 +2493,6 @@ export const SidebarChat = () => {
|
|||
role: 'assistant',
|
||||
displayContent: displayContentSoFar ?? '',
|
||||
reasoning: reasoningSoFar ?? '',
|
||||
toolCall: toolCallSoFar,
|
||||
anthropicReasoning: null,
|
||||
}}
|
||||
messageIdx={streamingChatIdx}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -29,6 +29,32 @@ import { IChatThreadService } from './chatThreadService.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 +89,6 @@ export const roundRangeToLines = (range: IRange | null | undefined, options: { e
|
|||
// }
|
||||
|
||||
|
||||
const findStagingItemToReplace = (currentSelections: StagingSelectionItem[] | undefined, newSelection: StagingSelectionItem): [number, StagingSelectionItem] | null => {
|
||||
if (!currentSelections) return null
|
||||
|
||||
for (let i = 0; i < currentSelections.length; i += 1) {
|
||||
const s = currentSelections[i]
|
||||
|
||||
if (s.uri.fsPath !== newSelection.uri.fsPath) continue
|
||||
|
||||
if (s.type === 'File' && newSelection.type === 'File') {
|
||||
return [i, s] as const
|
||||
}
|
||||
if (s.type === 'CodeSelection' && newSelection.type === 'CodeSelection') {
|
||||
if (s.uri.fsPath !== newSelection.uri.fsPath) continue
|
||||
// if there's any collision return true
|
||||
const [oldStart, oldEnd] = s.range
|
||||
const [newStart, newEnd] = newSelection.range
|
||||
if (oldStart !== newStart || oldEnd !== newEnd) continue
|
||||
return [i, s] as const
|
||||
}
|
||||
if (s.type === 'Folder' && newSelection.type === 'Folder') {
|
||||
return [i, s] as const
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const VOID_OPEN_SIDEBAR_ACTION_ID = 'void.sidebar.open'
|
||||
registerAction2(class extends Action2 {
|
||||
|
|
@ -132,7 +133,7 @@ registerAction2(class extends Action2 {
|
|||
}
|
||||
|
||||
|
||||
const selection: StagingSelectionItem = !selectionRange || (selectionRange.startLineNumber > selectionRange.endLineNumber) ? {
|
||||
const newSelection: StagingSelectionItem = !selectionRange || (selectionRange.startLineNumber > selectionRange.endLineNumber) ? {
|
||||
type: 'File',
|
||||
uri: model.uri,
|
||||
language: model.getLanguageId(),
|
||||
|
|
@ -163,21 +164,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])
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
@ -276,8 +276,8 @@ export class ToolsService implements IToolsService {
|
|||
const toIdx = MAX_FILE_CHARS_PAGE * pageNumber - 1
|
||||
const fileContents = contents.slice(fromIdx, toIdx + 1) // paginate
|
||||
const hasNextPage = (contents.length - 1) - toIdx >= 1
|
||||
|
||||
return { result: { fileContents, hasNextPage } }
|
||||
const totalFileLen = contents.length
|
||||
return { result: { fileContents, totalFileLen, hasNextPage } }
|
||||
},
|
||||
|
||||
ls_dir: async ({ rootURI, pageNumber }) => {
|
||||
|
|
@ -400,7 +400,7 @@ export class ToolsService implements IToolsService {
|
|||
// given to the LLM after the call
|
||||
this.stringOfResult = {
|
||||
read_file: (params, result) => {
|
||||
return result.fileContents + nextPageStr(result.hasNextPage)
|
||||
return `${result.fileContents}${nextPageStr(result.hasNextPage)}${result.hasNextPage ? `This file has ${result.totalFileLen} characters, paginated ${MAX_FILE_CHARS_PAGE} at a time.` : ''}`
|
||||
},
|
||||
ls_dir: (params, result) => {
|
||||
const dirTreeStr = stringifyDirectoryTree1Deep(params, result)
|
||||
|
|
@ -471,9 +471,10 @@ export class ToolsService implements IToolsService {
|
|||
private _getLintErrors(uri: URI): { lintErrors: LintErrorItem[] | null } {
|
||||
const lintErrors = this.markerService
|
||||
.read({ resource: uri })
|
||||
.filter(l => l.severity === MarkerSeverity.Error || l.severity === MarkerSeverity.Warning)
|
||||
.map(l => ({
|
||||
code: typeof l.code === 'string' ? l.code : l.code?.value || '',
|
||||
message: l.message,
|
||||
message: (l.severity === MarkerSeverity.Error ? '(error) ' : '(warning) ') + l.message,
|
||||
startLineNumber: l.startLineNumber,
|
||||
endLineNumber: l.endLineNumber,
|
||||
} satisfies LintErrorItem))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -130,6 +130,7 @@ export type VoidStaticModelInfo = { // not stateful
|
|||
}
|
||||
|
||||
supportsSystemMessage: false | 'system-role' | 'developer-role' | 'separated'; // separated = anthropic where "system" is a special paramete
|
||||
specialToolFormat?: 'openai-style' | 'anthropic-style', // null defaults to XML
|
||||
supportsFIM: boolean;
|
||||
|
||||
reasoningCapabilities: false | {
|
||||
|
|
@ -377,6 +378,7 @@ const anthropicModelOptions = {
|
|||
cost: { input: 3.00, cache_read: 0.30, cache_write: 3.75, output: 15.00 },
|
||||
downloadable: false,
|
||||
supportsFIM: false,
|
||||
specialToolFormat: 'anthropic-style',
|
||||
supportsSystemMessage: 'separated',
|
||||
reasoningCapabilities: {
|
||||
supportsReasoning: true,
|
||||
|
|
@ -385,6 +387,7 @@ const anthropicModelOptions = {
|
|||
reasoningMaxOutputTokens: 64_000, // can bump it to 128_000 with beta mode output-128k-2025-02-19
|
||||
reasoningBudgetSlider: { type: 'slider', min: 1024, max: 32_000, default: 1024 }, // they recommend batching if max > 32_000
|
||||
},
|
||||
|
||||
},
|
||||
'claude-3-5-sonnet-20241022': {
|
||||
contextWindow: 200_000,
|
||||
|
|
@ -392,6 +395,7 @@ const anthropicModelOptions = {
|
|||
cost: { input: 3.00, cache_read: 0.30, cache_write: 3.75, output: 15.00 },
|
||||
downloadable: false,
|
||||
supportsFIM: false,
|
||||
specialToolFormat: 'anthropic-style',
|
||||
supportsSystemMessage: 'separated',
|
||||
reasoningCapabilities: false,
|
||||
},
|
||||
|
|
@ -401,6 +405,7 @@ const anthropicModelOptions = {
|
|||
cost: { input: 0.80, cache_read: 0.08, cache_write: 1.00, output: 4.00 },
|
||||
downloadable: false,
|
||||
supportsFIM: false,
|
||||
specialToolFormat: 'anthropic-style',
|
||||
supportsSystemMessage: 'separated',
|
||||
reasoningCapabilities: false,
|
||||
},
|
||||
|
|
@ -410,6 +415,7 @@ const anthropicModelOptions = {
|
|||
cost: { input: 15.00, cache_read: 1.50, cache_write: 18.75, output: 75.00 },
|
||||
downloadable: false,
|
||||
supportsFIM: false,
|
||||
specialToolFormat: 'anthropic-style',
|
||||
supportsSystemMessage: 'separated',
|
||||
reasoningCapabilities: false,
|
||||
},
|
||||
|
|
@ -418,6 +424,7 @@ const anthropicModelOptions = {
|
|||
downloadable: false,
|
||||
maxOutputTokens: 4_096,
|
||||
supportsFIM: false,
|
||||
specialToolFormat: 'anthropic-style',
|
||||
supportsSystemMessage: 'separated',
|
||||
reasoningCapabilities: false,
|
||||
}
|
||||
|
|
@ -457,6 +464,7 @@ const openAIModelOptions = { // https://platform.openai.com/docs/pricing
|
|||
cost: { input: 2.00, output: 8.00, cache_read: 0.50 },
|
||||
downloadable: false,
|
||||
supportsFIM: false,
|
||||
specialToolFormat: 'openai-style',
|
||||
supportsSystemMessage: 'developer-role',
|
||||
reasoningCapabilities: false,
|
||||
},
|
||||
|
|
@ -466,6 +474,7 @@ const openAIModelOptions = { // https://platform.openai.com/docs/pricing
|
|||
cost: { input: 0.40, output: 1.60, cache_read: 0.10 },
|
||||
downloadable: false,
|
||||
supportsFIM: false,
|
||||
specialToolFormat: 'openai-style',
|
||||
supportsSystemMessage: 'developer-role',
|
||||
reasoningCapabilities: false,
|
||||
},
|
||||
|
|
@ -475,6 +484,7 @@ const openAIModelOptions = { // https://platform.openai.com/docs/pricing
|
|||
cost: { input: 0.10, output: 0.40, cache_read: 0.03 },
|
||||
downloadable: false,
|
||||
supportsFIM: false,
|
||||
specialToolFormat: 'openai-style',
|
||||
supportsSystemMessage: 'developer-role',
|
||||
reasoningCapabilities: false,
|
||||
},
|
||||
|
|
@ -502,6 +512,7 @@ const openAIModelOptions = { // https://platform.openai.com/docs/pricing
|
|||
cost: { input: 2.50, cache_read: 1.25, output: 10.00, },
|
||||
downloadable: false,
|
||||
supportsFIM: false,
|
||||
specialToolFormat: 'openai-style',
|
||||
supportsSystemMessage: 'system-role',
|
||||
reasoningCapabilities: false,
|
||||
},
|
||||
|
|
@ -520,6 +531,7 @@ const openAIModelOptions = { // https://platform.openai.com/docs/pricing
|
|||
cost: { input: 0.15, cache_read: 0.075, output: 0.60, },
|
||||
downloadable: false,
|
||||
supportsFIM: false,
|
||||
specialToolFormat: 'openai-style',
|
||||
supportsSystemMessage: 'system-role', // ??
|
||||
reasoningCapabilities: false,
|
||||
},
|
||||
|
|
@ -550,6 +562,15 @@ const xAIModelOptions = {
|
|||
supportsSystemMessage: 'system-role',
|
||||
reasoningCapabilities: false,
|
||||
},
|
||||
// 'grok-3': {
|
||||
// contextWindow: 1_000_000,
|
||||
// maxOutputTokens: null,
|
||||
// cost: {},
|
||||
// downloadable: false,
|
||||
// supportsFIM: false,
|
||||
// supportsSystemMessage: 'system-role',
|
||||
// reasoningCapabilities: {canIOReasoning:false, canTurnOffReasoning:true,},
|
||||
// }
|
||||
} as const satisfies { [s: string]: VoidStaticModelInfo }
|
||||
|
||||
const xAISettings: VoidStaticProviderInfo = {
|
||||
|
|
@ -1032,6 +1053,14 @@ export const getIsReasoningEnabledState = (
|
|||
}
|
||||
|
||||
|
||||
export const getMaxOutputTokens = (providerName: ProviderName, modelName: string, opts: { isReasoningEnabled: boolean }) => {
|
||||
const {
|
||||
reasoningCapabilities,
|
||||
maxOutputTokens
|
||||
} = getModelCapabilities(providerName, modelName)
|
||||
return opts.isReasoningEnabled && reasoningCapabilities ? reasoningCapabilities.reasoningMaxOutputTokens : maxOutputTokens
|
||||
}
|
||||
|
||||
// used to force reasoning state (complex) into something simple we can just read from when sending a message
|
||||
export const getSendableReasoningInfo = (
|
||||
featureName: FeatureName,
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
import { EndOfLinePreference } from '../../../../../editor/common/model.js';
|
||||
import { StagingSelectionItem } from '../chatThreadServiceTypes.js';
|
||||
import { os } from '../helpers/systemInfo.js';
|
||||
import { RawToolCallObj } from '../sendLLMMessageTypes.js';
|
||||
import { RawToolParamsObj } from '../sendLLMMessageTypes.js';
|
||||
import { toolNamesThatRequireApproval } from '../toolsServiceTypes.js';
|
||||
import { IVoidModelService } from '../voidModelService.js';
|
||||
import { ChatMode } from '../voidSettingsTypes.js';
|
||||
|
|
@ -218,12 +218,11 @@ Format:
|
|||
}).join('\n\n')}`
|
||||
}
|
||||
|
||||
export const toolCallXMLStr = (toolCall: RawToolCallObj) => {
|
||||
const t = toolCall
|
||||
const params = Object.keys(t.rawParams).map(paramName => `<${paramName}>${t.rawParams[paramName as ToolParamName]}</${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 +230,7 @@ export const toolCallXMLStr = (toolCall: RawToolCallObj) => {
|
|||
// - You are allowed to call multiple tools by specifying them consecutively. However, there should be NO text or writing between tool calls or after them.
|
||||
const systemToolsXMLPrompt = (chatMode: ChatMode) => {
|
||||
const tools = availableTools(chatMode)
|
||||
if (!tools || tools.length === 0) return ''
|
||||
if (!tools || tools.length === 0) return null
|
||||
|
||||
const toolXMLDefinitions = (`\
|
||||
Available tools:
|
||||
|
|
@ -255,7 +254,7 @@ ${toolCallXMLGuidelines}`
|
|||
// ======================================================== chat (normal, gather, agent) ========================================================
|
||||
|
||||
|
||||
export const chat_systemMessage = ({ workspaceFolders, openedURIs, activeURI, runningTerminalIds, directoryStr, chatMode: mode }: { workspaceFolders: string[], directoryStr: string, openedURIs: string[], activeURI: string | undefined, runningTerminalIds: string[], chatMode: ChatMode }) => {
|
||||
export const chat_systemMessage = ({ workspaceFolders, openedURIs, activeURI, runningTerminalIds, directoryStr, chatMode: mode, includeXMLToolDefinitions }: { workspaceFolders: string[], directoryStr: string, openedURIs: string[], activeURI: string | undefined, runningTerminalIds: string[], chatMode: ChatMode, includeXMLToolDefinitions: boolean }) => {
|
||||
const header = (`You are an expert coding ${mode === 'agent' ? 'agent' : 'assistant'} whose job is \
|
||||
${mode === 'agent' ? `to help the user develop, run, and make changes to their codebase.`
|
||||
: mode === 'gather' ? `to search, understand, and reference files in the user's codebase.`
|
||||
|
|
@ -289,7 +288,7 @@ ${directoryStr}
|
|||
</files_overview>`)
|
||||
|
||||
|
||||
const toolDefinitions = systemToolsXMLPrompt(mode)
|
||||
const toolDefinitions = includeXMLToolDefinitions ? systemToolsXMLPrompt(mode) : null
|
||||
|
||||
const details: string[] = []
|
||||
|
||||
|
|
@ -630,39 +629,6 @@ ${tripleTick[1]}).`
|
|||
|
||||
|
||||
|
||||
// const toAnthropicTool = (toolInfo: InternalToolInfo) => {
|
||||
// const { name, description, params } = toolInfo
|
||||
// return {
|
||||
// name: name,
|
||||
// description: description,
|
||||
// input_schema: {
|
||||
// type: 'object',
|
||||
// properties: params,
|
||||
// // required: Object.keys(params),
|
||||
// },
|
||||
// } satisfies Anthropic.Messages.Tool
|
||||
// }
|
||||
|
||||
|
||||
// const toOpenAICompatibleTool = (toolInfo: InternalToolInfo) => {
|
||||
// const { name, description, params } = toolInfo
|
||||
// return {
|
||||
// type: 'function',
|
||||
// function: {
|
||||
// name: name,
|
||||
// // strict: true, // strict mode - https://platform.openai.com/docs/guides/function-calling?api-mode=chat
|
||||
// description: description,
|
||||
// parameters: {
|
||||
// type: 'object',
|
||||
// properties: params,
|
||||
// // required: Object.keys(params), // in strict mode, all params are required and additionalProperties is false
|
||||
// // additionalProperties: false,
|
||||
// },
|
||||
// }
|
||||
// } satisfies OpenAI.Chat.Completions.ChatCompletionTool
|
||||
// }
|
||||
|
||||
|
||||
/*
|
||||
// ======================================================== ai search/replace ========================================================
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ export type ToolCallParams = {
|
|||
|
||||
// RESULT OF TOOL CALL
|
||||
export type ToolResultType = {
|
||||
'read_file': { fileContents: string, hasNextPage: boolean },
|
||||
'read_file': { fileContents: string, totalFileLen: number, hasNextPage: boolean },
|
||||
'ls_dir': { children: ShallowDirectoryItem[] | null, hasNextPage: boolean, hasPrevPage: boolean, itemsRemaining: number },
|
||||
'get_dir_structure': { str: string, },
|
||||
'search_pathnames_only': { uris: URI[], hasNextPage: boolean },
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
@ -199,11 +259,13 @@ const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage
|
|||
onText({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar })
|
||||
}
|
||||
// on final
|
||||
if (!fullTextSoFar && !fullReasoningSoFar) {
|
||||
if (!fullTextSoFar && !fullReasoningSoFar && !toolName) {
|
||||
onError({ message: 'Void: Response from model was empty.', fullError: null })
|
||||
}
|
||||
else {
|
||||
onFinalMessage({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar, anthropicReasoning: null });
|
||||
const toolCall = openAIToolToRawToolCallObj(toolName, toolParamsStr, toolId)
|
||||
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 +313,48 @@ const _openaiCompatibleList = async ({ onSuccess: onSuccess_, onError: onError_,
|
|||
|
||||
|
||||
|
||||
// ------------ ANTHROPIC (HELPERS) ------------
|
||||
const toAnthropicTool = (toolInfo: InternalToolInfo) => {
|
||||
const { name, description, params } = toolInfo
|
||||
return {
|
||||
name: name,
|
||||
description: description,
|
||||
input_schema: {
|
||||
type: 'object',
|
||||
properties: params,
|
||||
// required: Object.keys(params),
|
||||
},
|
||||
} satisfies Anthropic.Messages.Tool
|
||||
}
|
||||
|
||||
const anthropicTools = (chatMode: ChatMode) => {
|
||||
const allowedTools = availableTools(chatMode)
|
||||
if (!allowedTools || Object.keys(allowedTools).length === 0) return null
|
||||
|
||||
const anthropicTools: Anthropic.Messages.ToolUnion[] = []
|
||||
for (const t in allowedTools ?? {}) {
|
||||
anthropicTools.push(toAnthropicTool(allowedTools[t]))
|
||||
}
|
||||
return anthropicTools
|
||||
}
|
||||
|
||||
const anthropicToolToRawToolCallObj = (toolBlock: Anthropic.Messages.ToolUseBlock): RawToolCallObj | null => {
|
||||
const { id, name, input } = toolBlock
|
||||
if (!isAToolName(name)) return null
|
||||
const rawParams: RawToolParamsObj = {}
|
||||
if (input === null) return null
|
||||
if (typeof input !== 'object') return null
|
||||
for (const paramName in voidTools[name].params) {
|
||||
rawParams[paramName as ToolParamName] = (input as any)[paramName]
|
||||
}
|
||||
return { id, name, rawParams, doneParams: Object.keys(rawParams) as ToolParamName[], isDone: true }
|
||||
}
|
||||
|
||||
// ------------ ANTHROPIC ------------
|
||||
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 +365,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,8 +399,8 @@ const sendAnthropicChat = ({ messages: messages_, providerName, onText, onFinalM
|
|||
let fullText = ''
|
||||
let fullReasoning = ''
|
||||
|
||||
// let fullToolName = ''
|
||||
// let fullToolParams = ''
|
||||
let fullToolName = ''
|
||||
let fullToolParams = ''
|
||||
|
||||
// there are no events for tool_use, it comes in at the end
|
||||
stream.on('streamEvent', e => {
|
||||
|
|
@ -320,10 +422,10 @@ const sendAnthropicChat = ({ messages: messages_, providerName, onText, onFinalM
|
|||
fullReasoning += '[redacted_thinking]'
|
||||
onText({ fullText, fullReasoning, })
|
||||
}
|
||||
// else if (e.content_block.type === 'tool_use') {
|
||||
// fullToolName += e.content_block.name ?? '' // anthropic gives us the tool name in the start block
|
||||
// onText({ fullText, fullReasoning, })
|
||||
// }
|
||||
else if (e.content_block.type === 'tool_use') {
|
||||
fullToolName += e.content_block.name ?? '' // anthropic gives us the tool name in the start block
|
||||
onText({ fullText, fullReasoning, })
|
||||
}
|
||||
}
|
||||
|
||||
// delta
|
||||
|
|
@ -336,17 +438,20 @@ const sendAnthropicChat = ({ messages: messages_, providerName, onText, onFinalM
|
|||
fullReasoning += e.delta.thinking
|
||||
onText({ fullText, fullReasoning, })
|
||||
}
|
||||
// else if (e.delta.type === 'input_json_delta') { // tool use
|
||||
// fullToolParams += e.delta.partial_json ?? '' // anthropic gives us the partial delta (string) here - https://docs.anthropic.com/en/api/messages-streaming
|
||||
// onText({ fullText, fullReasoning, })
|
||||
// }
|
||||
else if (e.delta.type === 'input_json_delta') { // tool use
|
||||
fullToolParams += e.delta.partial_json ?? '' // anthropic gives us the partial delta (string) here - https://docs.anthropic.com/en/api/messages-streaming
|
||||
onText({ fullText, fullReasoning, })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// on done - (or when error/fail) - this is called AFTER last streamEvent
|
||||
stream.on('finalMessage', (response) => {
|
||||
const anthropicReasoning = response.content.filter(c => c.type === 'thinking' || c.type === 'redacted_thinking')
|
||||
onFinalMessage({ fullText, fullReasoning, anthropicReasoning })
|
||||
const tools = response.content.filter(c => c.type === 'tool_use')
|
||||
const toolCall = tools[0] && anthropicToolToRawToolCallObj(tools[0])
|
||||
const toolCallObj = toolCall ? { toolCall } : {}
|
||||
onFinalMessage({ fullText, fullReasoning, anthropicReasoning, ...toolCallObj })
|
||||
})
|
||||
// on error
|
||||
stream.on('error', (error) => {
|
||||
|
|
@ -360,7 +465,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 +474,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 +482,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 +530,10 @@ const ollamaList = async ({ onSuccess: onSuccess_, onError: onError_, settingsOf
|
|||
}
|
||||
}
|
||||
|
||||
const sendOllamaFIM = ({ messages: messages_, onFinalMessage, onError, settingsOfProvider, modelName, aiInstructions, _setAborter }: SendFIMParams_Internal) => {
|
||||
const sendOllamaFIM = ({ messages, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }: SendFIMParams_Internal) => {
|
||||
const thisConfig = settingsOfProvider.ollama
|
||||
const ollama = newOllamaSDK({ endpoint: thisConfig.endpoint })
|
||||
|
||||
const messages = prepareFIMMessage({ messages: messages_, aiInstructions, })
|
||||
|
||||
let fullText = ''
|
||||
ollama.generate({
|
||||
model: modelName,
|
||||
|
|
@ -439,7 +541,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 })
|
||||
|
|
|
|||
Loading…
Reference in a new issue