diff --git a/package-lock.json b/package-lock.json index 513936cc..a365be59 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "@anthropic-ai/sdk": "^0.40.0", "@c4312/eventsource-umd": "^3.0.5", "@floating-ui/react": "^0.27.8", - "@google/generative-ai": "^0.24.0", + "@google/generative-ai": "^0.24.1", "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", "@mistralai/mistralai": "^1.6.0", @@ -1817,9 +1817,9 @@ "license": "MIT" }, "node_modules/@google/generative-ai": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@google/generative-ai/-/generative-ai-0.24.0.tgz", - "integrity": "sha512-fnEITCGEB7NdX0BhoYZ/cq/7WPZ1QS5IzJJfC3Tg/OwkvBetMiVJciyaan297OvE4B9Jg1xvo0zIazX/9sGu1Q==", + "version": "0.24.1", + "resolved": "https://registry.npmjs.org/@google/generative-ai/-/generative-ai-0.24.1.tgz", + "integrity": "sha512-MqO+MLfM6kjxcKoy0p1wRzG3b4ZZXtPI+z2IE26UogS2Cm/XHO+7gGRBh6gcJsOiIVoH93UwKvW4HdgiOZCy9Q==", "license": "Apache-2.0", "engines": { "node": ">=18.0.0" diff --git a/package.json b/package.json index 23de71cf..462cf549 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,7 @@ "@anthropic-ai/sdk": "^0.40.0", "@c4312/eventsource-umd": "^3.0.5", "@floating-ui/react": "^0.27.8", - "@google/generative-ai": "^0.24.0", + "@google/generative-ai": "^0.24.1", "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", "@mistralai/mistralai": "^1.6.0", diff --git a/src/vs/workbench/contrib/void/browser/convertToLLMMessageService.ts b/src/vs/workbench/contrib/void/browser/convertToLLMMessageService.ts index 65b45d33..c19ed1c7 100644 --- a/src/vs/workbench/contrib/void/browser/convertToLLMMessageService.ts +++ b/src/vs/workbench/contrib/void/browser/convertToLLMMessageService.ts @@ -8,9 +8,9 @@ import { IEditorService } from '../../../services/editor/common/editorService.js import { ChatMessage } from '../common/chatThreadServiceTypes.js'; import { getIsReasoningEnabledState, getMaxOutputTokens, getModelCapabilities } from '../common/modelCapabilities.js'; import { reParsedToolXMLString, chat_systemMessage, ToolName } from '../common/prompt/prompts.js'; -import { AnthropicLLMChatMessage, AnthropicReasoning, LLMChatMessage, LLMFIMMessage, OpenAILLMChatMessage, RawToolParamsObj } from '../common/sendLLMMessageTypes.js'; +import { AnthropicLLMChatMessage, AnthropicReasoning, GeminiLLMChatMessage, LLMChatMessage, LLMFIMMessage, OpenAILLMChatMessage, RawToolParamsObj } from '../common/sendLLMMessageTypes.js'; import { IVoidSettingsService } from '../common/voidSettingsService.js'; -import { ChatMode, FeatureName, ModelSelection } from '../common/voidSettingsTypes.js'; +import { ChatMode, FeatureName, ModelSelection, ProviderName } from '../common/voidSettingsTypes.js'; import { IDirectoryStrService } from './directoryStrService.js'; import { ITerminalToolService } from './terminalToolService.js'; import { IVoidModelService } from '../common/voidModelService.js'; @@ -36,8 +36,6 @@ type SimpleLLMMessage = { } - - const EMPTY_MESSAGE = '(empty message)' const CHARS_PER_TOKEN = 4 @@ -69,7 +67,7 @@ openai on developer system message - https://cdn.openai.com/spec/model-spec-2024 */ -const prepareMessages_openai_tools = (messages: SimpleLLMMessage[]): LLMChatMessage[] => { +const prepareMessages_openai_tools = (messages: SimpleLLMMessage[]): AnthropicOrOpenAILLMMessage[] => { const newMessages: OpenAILLMChatMessage[] = []; @@ -136,8 +134,9 @@ assistant: ...content, call(name, id, params) user: ...content, result(id, content) */ +type AnthropicOrOpenAILLMMessage = AnthropicLLMChatMessage | OpenAILLMChatMessage -const prepareMessages_anthropic_tools = (messages: SimpleLLMMessage[], supportsAnthropicReasoning: boolean): LLMChatMessage[] => { +const prepareMessages_anthropic_tools = (messages: SimpleLLMMessage[], supportsAnthropicReasoning: boolean): AnthropicOrOpenAILLMMessage[] => { const newMessages: (AnthropicLLMChatMessage | (SimpleLLMMessage & { role: 'tool' }))[] = messages; for (let i = 0; i < messages.length; i += 1) { @@ -195,9 +194,9 @@ const prepareMessages_anthropic_tools = (messages: SimpleLLMMessage[], supportsA } -const prepareMessages_XML_tools = (messages: SimpleLLMMessage[], supportsAnthropicReasoning: boolean): LLMChatMessage[] => { +const prepareMessages_XML_tools = (messages: SimpleLLMMessage[], supportsAnthropicReasoning: boolean): AnthropicOrOpenAILLMMessage[] => { - const llmChatMessages: LLMChatMessage[] = []; + const llmChatMessages: AnthropicOrOpenAILLMMessage[] = []; for (let i = 0; i < messages.length; i += 1) { const c = messages[i] @@ -206,7 +205,7 @@ const prepareMessages_XML_tools = (messages: SimpleLLMMessage[], supportsAnthrop 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 + let content: AnthropicOrOpenAILLMMessage['content'] = c.content if (next?.role === 'tool') { content = `${content}\n\n${reParsedToolXMLString(next.name, next.rawParams)}` } @@ -239,24 +238,20 @@ const prepareMessages_XML_tools = (messages: SimpleLLMMessage[], supportsAnthrop -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 -} + +export type GeminiMessage = { + role: 'user' | 'model'; // Gemini uses 'user' and 'model' roles + parts: ( + | { text: string; } + | { functionCall: { tool_call: any } } + | { functionResponse: { name: ToolName, response: { result: string } } } + )[]; +}; // --- CHAT --- -const prepareMessages = ({ +const prepareOpenAIOrAnthropicMessages = ({ messages, systemMessage, aiInstructions, @@ -274,7 +269,7 @@ const prepareMessages = ({ supportsAnthropicReasoning: boolean, contextWindow: number, maxOutputTokens: number | null | undefined, -}): { messages: LLMChatMessage[], separateSystemMessage: string | undefined } => { +}): { messages: AnthropicOrOpenAILLMMessage[], separateSystemMessage: string | undefined } => { maxOutputTokens = maxOutputTokens ?? 4_096 // default to 4096 // ================ trim ================ @@ -350,7 +345,19 @@ const prepareMessages = ({ } // ================ tools and anthropicReasoning ================ - const llmMessages: LLMChatMessage[] = prepareMessages_providerSpecific(messages, specialToolFormat, supportsAnthropicReasoning) + + let llmChatMessages: AnthropicOrOpenAILLMMessage[] = [] + if (!specialToolFormat) { // XML tool behavior + llmChatMessages = prepareMessages_XML_tools(messages, supportsAnthropicReasoning) + } + else if (specialToolFormat === 'anthropic-style') { + llmChatMessages = prepareMessages_anthropic_tools(messages, supportsAnthropicReasoning) + } + else if (specialToolFormat === 'openai-style') { + llmChatMessages = prepareMessages_openai_tools(messages) + } + const llmMessages = llmChatMessages + // ================ system message concat ================ @@ -406,19 +413,92 @@ const prepareMessages = ({ +type GeminiUserPart = (GeminiLLMChatMessage & { role: 'user' })['parts'][0] +type GeminiModelPart = (GeminiLLMChatMessage & { role: 'model' })['parts'][0] +const prepareGeminiMessages = (messages: AnthropicLLMChatMessage[]) => { + let latestToolName: ToolName | undefined = undefined + const messages2: GeminiLLMChatMessage[] = messages.map((m): GeminiLLMChatMessage | null => { + if (m.role === 'assistant') { + if (typeof m.content === 'string') { + return { role: 'model', parts: [{ text: m.content }] } + } + else { + const parts: GeminiModelPart[] = m.content.map((c): GeminiModelPart | null => { + if (c.type === 'text') { + return { text: c.text } + } + else if (c.type === 'tool_use') { + latestToolName = c.name as ToolName + return { functionCall: { name: c.name as ToolName, args: c.input } } + } + else return null + }).filter(m => !!m) + return { role: 'model', parts, } + } + } + else if (m.role === 'user') { + if (typeof m.content === 'string') { + return { role: 'user', parts: [{ text: m.content }] } satisfies GeminiLLMChatMessage + } + else { + const parts: GeminiUserPart[] = m.content.map((c): GeminiUserPart | null => { + if (c.type === 'text') { + return { text: c.text } + } + else if (c.type === 'tool_result') { + if (!latestToolName) return null + return { functionResponse: { name: latestToolName, response: { result: c.content } } } + } + else return null + }).filter(m => !!m) + return { role: 'user', parts, } + } + + } + else return null + }).filter(m => !!m) + + return messages2 +} +const prepareMessages = (params: { + messages: SimpleLLMMessage[], + systemMessage: string, + aiInstructions: string, + supportsSystemMessage: false | 'system-role' | 'developer-role' | 'separated', + specialToolFormat: 'openai-style' | 'anthropic-style' | 'gemini-style' | undefined, + supportsAnthropicReasoning: boolean, + contextWindow: number, + maxOutputTokens: number | null | undefined, + providerName: ProviderName +}): { messages: LLMChatMessage[], separateSystemMessage: string | undefined } => { + const specialFormat = params.specialToolFormat // this is just for ts idiocy + if (params.providerName === 'gemini') { + // treat as anthropic style, then convert to gemini style + const res = prepareOpenAIOrAnthropicMessages({ ...params, specialToolFormat: specialFormat === 'gemini-style' ? 'anthropic-style' : undefined }) + const messages = res.messages as AnthropicLLMChatMessage[] + const messages2 = prepareGeminiMessages(messages) + return { messages: messages2, separateSystemMessage: res.separateSystemMessage } + } + else { + if (specialFormat === 'gemini-style') { + throw new Error(`Tried preparing messages with tool format ${params.specialToolFormat} but the provider was ${params.providerName}, not Gemini.`) + } + } + + return prepareOpenAIOrAnthropicMessages({ ...params, specialToolFormat: specialFormat }) +} 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 }> + prepareLLMSimpleMessages: (opts: { simpleMessages: SimpleLLMMessage[], systemMessage: string, modelSelection: ModelSelection | null, featureName: FeatureName }) => { messages: LLMChatMessage[], separateSystemMessage: string | undefined, geminiMessages?: GeminiMessage[] } + prepareLLMChatMessages: (opts: { chatMessages: ChatMessage[], chatMode: ChatMode, modelSelection: ModelSelection | null }) => Promise<{ messages: LLMChatMessage[], separateSystemMessage: string | undefined, geminiMessages?: GeminiMessage[] }> prepareFIMMessage(opts: { messages: LLMFIMMessage, }): { prefix: string, suffix: string, stopTokens: string[] } - } export const IConvertToLLMMessageService = createDecorator('ConvertToLLMMessageService'); @@ -470,7 +550,7 @@ class ConvertToLLMMessageService extends Disposable implements IConvertToLLMMess // system message - private _generateChatMessagesSystemMessage = async (chatMode: ChatMode, specialToolFormat: 'openai-style' | 'anthropic-style' | undefined) => { + private _generateChatMessagesSystemMessage = async (chatMode: ChatMode, specialToolFormat: 'openai-style' | 'anthropic-style' | 'gemini-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) || []; @@ -551,6 +631,7 @@ class ConvertToLLMMessageService extends Disposable implements IConvertToLLMMess supportsAnthropicReasoning: providerName === 'anthropic', contextWindow, maxOutputTokens, + providerName, }) return { messages, separateSystemMessage }; } @@ -582,6 +663,7 @@ class ConvertToLLMMessageService extends Disposable implements IConvertToLLMMess supportsAnthropicReasoning: providerName === 'anthropic', contextWindow, maxOutputTokens, + providerName, }) return { messages, separateSystemMessage }; } diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index 1ae9c0aa..a106e601 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -14,8 +14,6 @@ import { ICodeEditorService } from '../../../../editor/browser/services/codeEdit import { findDiffs } from './helpers/findDiffs.js'; import { EndOfLinePreference, IModelDecorationOptions, ITextModel } from '../../../../editor/common/model.js'; import { IRange } from '../../../../editor/common/core/range.js'; -import { registerColor } from '../../../../platform/theme/common/colorUtils.js'; -import { Color, RGBA } from '../../../../base/common/color.js'; import { IModelService } from '../../../../editor/common/services/model.js'; import { IUndoRedoElement, IUndoRedoService, UndoRedoElementType } from '../../../../platform/undoRedo/common/undoRedo.js'; import { RenderOptions } from '../../../../editor/browser/widget/diffEditor/components/diffEditorViewZones/renderLines.js'; @@ -48,27 +46,6 @@ import { IConvertToLLMMessageService } from './convertToLLMMessageService.js'; // import { isMacintosh } from '../../../../base/common/platform.js'; // import { VOID_OPEN_SETTINGS_ACTION_ID } from './voidSettingsPane.js'; -const configOfBG = (color: Color) => { - return { dark: color, light: color, hcDark: color, hcLight: color, } -} -// gets converted to --vscode-void-greenBG, see void.css, asCssVariable -const greenBG = new Color(new RGBA(155, 185, 85, .2)); // default is RGBA(155, 185, 85, .2) -registerColor('void.greenBG', configOfBG(greenBG), '', true); - -const redBG = new Color(new RGBA(255, 0, 0, .2)); // default is RGBA(255, 0, 0, .2) -registerColor('void.redBG', configOfBG(redBG), '', true); - -const sweepBG = new Color(new RGBA(100, 100, 100, .2)); -registerColor('void.sweepBG', configOfBG(sweepBG), '', true); - -const highlightBG = new Color(new RGBA(100, 100, 100, .1)); -registerColor('void.highlightBG', configOfBG(highlightBG), '', true); - -const sweepIdxBG = new Color(new RGBA(100, 100, 100, .5)); -registerColor('void.sweepIdxBG', configOfBG(sweepIdxBG), '', true); - - - const numLinesOfStr = (str: string) => str.split('\n').length @@ -129,10 +106,10 @@ const removeWhitespaceExceptNewlines = (str: string): string => { // finds block.orig in fileContents and return its range in file // startingAtLine is 1-indexed and inclusive -const findTextInCode = (text: string, fileContents: string, canFallbackToRemoveWhitespace: boolean, startingAtLine?: number) => { +const findTextInCode = (text: string, fileContents: string, canFallbackToRemoveWhitespace: boolean, opts: { startingAtLine?: number, returnType: 'lines' | 'indices' }) => { - const startLineIdx = (fileContents: string) => startingAtLine !== undefined ? - fileContents.split('\n').slice(0, startingAtLine).join('\n').length // num characters in all lines before startingAtLine + const startLineIdx = (fileContents: string) => opts?.startingAtLine !== undefined ? + fileContents.split('\n').slice(0, opts.startingAtLine).join('\n').length // num characters in all lines before startingAtLine : 0 // idx = starting index in fileContents @@ -148,10 +125,18 @@ const findTextInCode = (text: string, fileContents: string, canFallbackToRemoveW if (idx === -1) return 'Not found' as const const lastIdx = fileContents.lastIndexOf(text) if (lastIdx !== idx) return 'Not unique' as const - const startLine = fileContents.substring(0, idx).split('\n').length - const numLines = numLinesOfStr(text) - const endLine = startLine + numLines - 1 - return [startLine, endLine] as const + + if (opts.returnType === 'lines') { + const startLine = fileContents.substring(0, idx).split('\n').length + const numLines = numLinesOfStr(text) + const endLine = startLine + numLines - 1 + return [startLine, endLine] as const + } + + else if (opts.returnType === 'indices') { + return [idx, idx + text.length] as const + } + else throw new Error(`findTextInCode: Invalid returnType ${opts.returnType}`) } @@ -1573,14 +1558,14 @@ class EditCodeService extends Disposable implements IEditCodeService { private _errContentOfInvalidStr = (str: 'Not found' | 'Not unique' | 'Has overlap', blockOrig: string) => { - const problematicCode = `The problematic ORIGINAL code was:\n${tripleTick[0]}\n${JSON.stringify(blockOrig)}\n${tripleTick[1]}` + const problematicCode = `${tripleTick[0]}\n${JSON.stringify(blockOrig)}\n${tripleTick[1]}` const descStr = str === `Not found` ? - `The most recent ORIGINAL code could not be found in the file, so you were interrupted. The text in ORIGINAL must EXACTLY match lines of code. ${problematicCode}` + `The edit was not applied. The text in ORIGINAL must EXACTLY match lines of code in the file, but there was no match for:\n${problematicCode}. Ensure you have the latest version of the file, and ensure the ORIGINAL code matches a code excerpt exactly.` : str === `Not unique` ? - `The most recent ORIGINAL code shows up multiple times in the file, so you were interrupted. You might want to expand the ORIGINAL excerpt so it's unique. ${problematicCode}` + `The edit was not applied. The text in ORIGINAL must be unique, but the following ORIGINAL code appears multiple times in the file:\n${problematicCode}. Ensure you have the latest version of the file, and ensure the ORIGINAL code is unique.` : str === 'Has overlap' ? - `The most recent ORIGINAL code has overlap with another ORIGINAL code block that you outputted. Do NOT output any overlapping edits. ${problematicCode}` + `The edit was not applied. The text in the ORIGINAL blocks must not overlap, but the following ORIGINAL code had overlap with another ORIGINAL string:\n${problematicCode}. Ensure you have the latest version of the file, and ensure the ORIGINAL code blocks do not overlap.` : `` return descStr } @@ -1594,14 +1579,13 @@ class EditCodeService extends Disposable implements IEditCodeService { if (!model) throw new Error(`Error applying Search/Replace blocks: File does not exist.`) const modelStr = model.getValue(EndOfLinePreference.LF) + const replacements: { origStart: number; origEnd: number; block: ExtractedSearchReplaceBlock }[] = [] for (const b of blocks) { - const i = modelStr.indexOf(b.orig) - if (i === -1) - throw new Error(this._errContentOfInvalidStr('Not found', b.orig)) - const j = modelStr.lastIndexOf(b.orig) - if (i !== j) - throw new Error(this._errContentOfInvalidStr('Not unique', b.orig)) + const res = findTextInCode(b.orig, modelStr, true, { returnType: 'indices' }) + if (typeof res === 'string') + throw new Error(this._errContentOfInvalidStr(res, b.orig)) + const [i, _] = res replacements.push({ origStart: i, @@ -1772,7 +1756,7 @@ class EditCodeService extends Disposable implements IEditCodeService { // update stream state to the first line of original if some portion of original has been written if (shouldUpdateOrigStreamStyle && block.orig.trim().length >= 20) { const startingAtLine = diffZone._streamState.line ?? 1 // dont go backwards if already have a stream line - const originalRange = findTextInCode(block.orig, originalFileCode, false, startingAtLine) + const originalRange = findTextInCode(block.orig, originalFileCode, false, { startingAtLine, returnType: 'lines' }) if (typeof originalRange !== 'string') { const [startLine, _] = convertOriginalRangeToFinalRange(originalRange) diffZone._streamState.line = startLine @@ -1798,7 +1782,7 @@ class EditCodeService extends Disposable implements IEditCodeService { // if this is the first time we're seeing this block, add it as a diffarea so we can start streaming in it if (!(blockNum in addedTrackingZoneOfBlockNum)) { - const originalBounds = findTextInCode(block.orig, originalFileCode, true) + const originalBounds = findTextInCode(block.orig, originalFileCode, true, { returnType: 'lines' }) // if error // Check for overlap with existing modified ranges const hasOverlap = addedTrackingZoneOfBlockNum.some(trackingZone => { diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx index 645359ef..5760b2ab 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx @@ -832,55 +832,39 @@ const EditTool = ({ toolMessage, threadId, messageIdx, content }: Parameters - } + const applyBoxId = getApplyBoxId({ + threadId: threadId, + messageIdx: messageIdx, + tokenIdx: 'N/A', + }) + componentParams.desc2 = // add children + componentParams.children = + + + if (toolMessage.type !== 'tool_error') { const { result } = toolMessage - - componentParams.bottomChildren = - - componentParams.children = - - + componentParams.bottomChildren = + {result?.lintErrors?.map((error, i) => ( +
Lines {error.startLineNumber}-{error.endLineNumber}: {error.message}
+ ))} +
} else { // error const { result } = toolMessage - if (params) { - componentParams.children = - {/* error */} - - {result} - - - {/* content */} - - - } - else { - componentParams.children = - {result} - - } + componentParams.bottomChildren = + {result} + } } @@ -1063,87 +1047,87 @@ const UserMessageComponent = ({ chatMessage, messageIdx, isCheckpointGhost, curr setSelections={setStagingSelections} > setIsDisabled(!text)} - onFocus={() => { - setIsFocused(true) - chatThreadsService.setCurrentlyFocusedMessageIdx(messageIdx); - }} - onBlur={() => { - setIsFocused(false) - }} - onKeyDown={onKeyDown} - fnsRef={textAreaFnsRef} - multiline={true} - /> - -} + enableAtToMention + ref={setTextAreaRef} + className='min-h-[81px] max-h-[500px] px-0.5' + placeholder="Edit your message..." + onChangeText={(text) => setIsDisabled(!text)} + onFocus={() => { + setIsFocused(true) + chatThreadsService.setCurrentlyFocusedMessageIdx(messageIdx); + }} + onBlur={() => { + setIsFocused(false) + }} + onKeyDown={onKeyDown} + fnsRef={textAreaFnsRef} + multiline={true} + /> + + } -const isMsgAfterCheckpoint = currCheckpointIdx !== undefined && currCheckpointIdx === messageIdx - 1 + const isMsgAfterCheckpoint = currCheckpointIdx !== undefined && currCheckpointIdx === messageIdx - 1 -return
setIsHovered(true)} - onMouseLeave={() => setIsHovered(false)} -> -
setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > +
{ if (mode === 'display') { onOpenEdit() } }} - > - {chatbubbleContents} -
+ onClick={() => { if (mode === 'display') { onOpenEdit() } }} + > + {chatbubbleContents} +
-
- + { - if (mode === 'display') { - onOpenEdit() - } else if (mode === 'edit') { - onCloseEdit() - } - }} - /> -
+ onClick={() => { + if (mode === 'display') { + onOpenEdit() + } else if (mode === 'edit') { + onCloseEdit() + } + }} + /> +
- + } const SmallProseWrapper = ({ children }: { children: React.ReactNode }) => { -return
- {children} -
+ {children} + } const ProseWrapper = ({ children }: { children: React.ReactNode }) => { -return
- {children} -
+ > + {children} + } const AssistantMessageComponent = ({ chatMessage, isCheckpointGhost, isCommitted, messageIdx }: { chatMessage: ChatMessage & { role: 'assistant' }, isCheckpointGhost: boolean, messageIdx: number, isCommitted: boolean }) => { -const accessor = useAccessor() -const chatThreadsService = accessor.get('IChatThreadService') + const accessor = useAccessor() + const chatThreadsService = accessor.get('IChatThreadService') -const reasoningStr = chatMessage.reasoning?.trim() || null -const hasReasoning = !!reasoningStr -const isDoneReasoning = !!chatMessage.displayContent -const thread = chatThreadsService.getCurrentThread() + const reasoningStr = chatMessage.reasoning?.trim() || null + const hasReasoning = !!reasoningStr + const isDoneReasoning = !!chatMessage.displayContent + const thread = chatThreadsService.getCurrentThread() -const chatMessageLocation: ChatMessageLocation = { - threadId: thread.id, - messageIdx: messageIdx, -} + const chatMessageLocation: ChatMessageLocation = { + threadId: thread.id, + messageIdx: messageIdx, + } -const isEmpty = !chatMessage.displayContent && !chatMessage.reasoning -if (isEmpty) return null + const isEmpty = !chatMessage.displayContent && !chatMessage.reasoning + if (isEmpty) return null -return <> - {/* reasoning token */} - {hasReasoning && -
- - - - - -
- } + return <> + {/* reasoning token */} + {hasReasoning && +
+ + + + + +
+ } - {/* assistant message */} - {chatMessage.displayContent && -
- - - -
- } - + {/* assistant message */} + {chatMessage.displayContent && +
+ + + +
+ } + } const ReasoningWrapper = ({ isDoneReasoning, isStreaming, children }: { isDoneReasoning: boolean, isStreaming: boolean, children: React.ReactNode }) => { -const isDone = isDoneReasoning || !isStreaming -const isWriting = !isDone -const [isOpen, setIsOpen] = useState(isWriting) -useEffect(() => { - if (!isWriting) setIsOpen(false) // if just finished reasoning, close -}, [isWriting]) -return : ''} isOpen={isOpen} onClick={() => setIsOpen(v => !v)}> - -
- {children} -
-
-
+ const isDone = isDoneReasoning || !isStreaming + const isWriting = !isDone + const [isOpen, setIsOpen] = useState(isWriting) + useEffect(() => { + if (!isWriting) setIsOpen(false) // if just finished reasoning, close + }, [isWriting]) + return : ''} isOpen={isOpen} onClick={() => setIsOpen(v => !v)}> + +
+ {children} +
+
+
} @@ -1309,10 +1293,10 @@ return : // should either be past or "-ing" tense, not present tense. Eg. when the LLM searches for something, the user expects it to say "I searched for X" or "I am searching for X". Not "I search X". const loadingTitleWrapper = (item: React.ReactNode): React.ReactNode => { -return - {item} - - + return + {item} + + } const titleOfToolName = { @@ -1422,7 +1406,7 @@ const toolNameToDesc = (toolName: ToolName, _toolParams: ToolCallParams[ToolName const toolParams = _toolParams as ToolCallParams['run_command'] return { desc1: `"${toolParams.command}"`, - } + } }, 'run_persistent_command': () => { const toolParams = _toolParams as ToolCallParams['run_persistent_command'] @@ -1577,8 +1561,8 @@ const LintErrorChildren = ({ lintErrors }: { lintErrors: LintErrorItem[] }) => { } -const EditToolLintErrors = ({ lintErrors }: { lintErrors: LintErrorItem[] }) => { - if (lintErrors.length === 0) return null; +const BottomChildren = ({ children, title }: { children: React.ReactNode, title: string }) => { + if (!children) return null; const [isOpen, setIsOpen] = useState(false); return (
@@ -1590,15 +1574,13 @@ const EditToolLintErrors = ({ lintErrors }: { lintErrors: LintErrorItem[] }) => - Lint errors + {title}
- {lintErrors.map((error, i) => ( -
Lines {error.startLineNumber}-{error.endLineNumber}: {error.message}
- ))} + {children}
@@ -2178,7 +2160,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, return } }, - 'rewrite_file': { + 'rewrite_file': { resultWrapper: (params) => { return } @@ -2202,7 +2184,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, return } }, - 'open_persistent_terminal': { + 'open_persistent_terminal': { resultWrapper: ({ toolMessage }) => { const accessor = useAccessor() const terminalToolsService = accessor.get('ITerminalToolService') @@ -2795,7 +2777,7 @@ export const SidebarChat = () => { const sidebarRef = useRef(null) const scrollContainerRef = useRef(null) - const onSubmit = useCallback(async (_forceSubmit?: string) => { + const onSubmit = useCallback(async (_forceSubmit?: string) => { if (isDisabled && !_forceSubmit) return if (isRunning) return @@ -2817,7 +2799,7 @@ export const SidebarChat = () => { }, [chatThreadsService, isDisabled, isRunning, textAreaRef, textAreaFnsRef, setSelections, settingsState]) - const onAbort = async () => { + const onAbort = async () => { const threadId = currentThread.id await chatThreadsService.abortRunning(threadId) } diff --git a/src/vs/workbench/contrib/void/browser/toolsService.ts b/src/vs/workbench/contrib/void/browser/toolsService.ts index 188bb658..8566db21 100644 --- a/src/vs/workbench/contrib/void/browser/toolsService.ts +++ b/src/vs/workbench/contrib/void/browser/toolsService.ts @@ -295,12 +295,14 @@ export class ToolsService implements IToolsService { contents = model.getValueInRange({ startLineNumber, startColumn: 1, endLineNumber, endColumn: Number.MAX_SAFE_INTEGER }, EndOfLinePreference.LF) } + const totalNumLines = model.getLineCount() + const fromIdx = MAX_FILE_CHARS_PAGE * (pageNumber - 1) const toIdx = MAX_FILE_CHARS_PAGE * pageNumber - 1 const fileContents = contents.slice(fromIdx, toIdx + 1) // paginate const hasNextPage = (contents.length - 1) - toIdx >= 1 const totalFileLen = contents.length - return { result: { fileContents, totalFileLen, hasNextPage } } + return { result: { fileContents, totalFileLen, hasNextPage, totalNumLines } } }, ls_dir: async ({ uri, pageNumber }) => { @@ -414,7 +416,6 @@ export class ToolsService implements IToolsService { if (this.commandBarService.getStreamState(uri) === 'streaming') { throw new Error(`Another LLM is currently making changes to this file. Please stop streaming for now and ask the user to resume later.`) } - console.log('aaaa', searchReplaceBlocks) editCodeService.instantlyApplySearchReplaceBlocks({ uri, searchReplaceBlocks }) // at end, get lint errors @@ -460,7 +461,7 @@ export class ToolsService implements IToolsService { // given to the LLM after the call for successful tool calls this.stringOfResult = { read_file: (params, result) => { - return `${params.uri.fsPath}\n\`\`\`\n${result.fileContents}\n\`\`\`${nextPageStr(result.hasNextPage)}` + return `${params.uri.fsPath}\n\`\`\`\n${result.fileContents}\n\`\`\`${nextPageStr(result.hasNextPage)}${result.hasNextPage ? `\nMore info because truncated: this file has ${result.totalNumLines} lines, or ${result.totalFileLen} characters.` : ''}` }, ls_dir: (params, result) => { const dirTreeStr = stringifyDirectoryTree1Deep(params, result) @@ -522,7 +523,7 @@ export class ToolsService implements IToolsService { } // normal command if (resolveReason.type === 'timeout') { - return `${result_}\nTerminal command ran, but was interrupted by Void after ${MAX_TERMINAL_INACTIVE_TIME}s of inactivity and did not necessarily finish successfully. To try with more time, open a persistent terminal and run the command there.` + return `${result_}\nTerminal command ran, but was automatically killed by Void after ${MAX_TERMINAL_INACTIVE_TIME}s of inactivity and did not finish successfully. To try with more time, open a persistent terminal and run the command there.` } throw new Error(`Unexpected internal error: Terminal command did not resolve with a valid reason.`) }, diff --git a/src/vs/workbench/contrib/void/common/helpers/colors.ts b/src/vs/workbench/contrib/void/common/helpers/colors.ts index 1a20ea9b..ffc5324f 100644 --- a/src/vs/workbench/contrib/void/common/helpers/colors.ts +++ b/src/vs/workbench/contrib/void/common/helpers/colors.ts @@ -1,3 +1,8 @@ +import { Color, RGBA } from '../../../../../base/common/color.js'; +import { registerColor } from '../../../../../platform/theme/common/colorUtils.js'; + + +// Widget colors export const acceptBg = '#1a7431' export const acceptAllBg = '#1e8538' export const acceptBorder = '1px solid #145626' @@ -6,3 +11,23 @@ export const rejectAllBg = '#cf2838' export const rejectBorder = '1px solid #8e1c27' export const buttonFontSize = '11px' export const buttonTextColor = 'white' + + +// editCodeService colors +export const greenBG = new Color(new RGBA(155, 185, 85, .1)); // default is RGBA(155, 185, 85, .2) +export const redBG = new Color(new RGBA(255, 0, 0, .1)); // default is RGBA(255, 0, 0, .2) +export const sweepBG = new Color(new RGBA(100, 100, 100, .2)); +export const highlightBG = new Color(new RGBA(100, 100, 100, .1)); +export const sweepIdxBG = new Color(new RGBA(100, 100, 100, .5)); + + +const configOfBG = (color: Color) => { + return { dark: color, light: color, hcDark: color, hcLight: color, } +} + +// gets converted to --vscode-void-greenBG, see void.css, asCssVariable +registerColor('void.greenBG', configOfBG(greenBG), '', true); +registerColor('void.redBG', configOfBG(redBG), '', true); +registerColor('void.sweepBG', configOfBG(sweepBG), '', true); +registerColor('void.highlightBG', configOfBG(highlightBG), '', true); +registerColor('void.sweepIdxBG', configOfBG(sweepIdxBG), '', true); diff --git a/src/vs/workbench/contrib/void/common/modelCapabilities.ts b/src/vs/workbench/contrib/void/common/modelCapabilities.ts index 48f9fb57..9e62168f 100644 --- a/src/vs/workbench/contrib/void/common/modelCapabilities.ts +++ b/src/vs/workbench/contrib/void/common/modelCapabilities.ts @@ -153,7 +153,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 + specialToolFormat?: 'openai-style' | 'anthropic-style' | 'gemini-style', // null defaults to XML supportsFIM: boolean; reasoningCapabilities: false | { @@ -648,6 +648,7 @@ const geminiModelOptions = { // https://ai.google.dev/gemini-api/docs/pricing downloadable: false, supportsFIM: false, supportsSystemMessage: 'system-role', + specialToolFormat: 'gemini-style', reasoningCapabilities: false, }, 'gemini-2.5-pro-exp-03-25': { @@ -657,6 +658,7 @@ const geminiModelOptions = { // https://ai.google.dev/gemini-api/docs/pricing downloadable: false, supportsFIM: false, supportsSystemMessage: 'system-role', + specialToolFormat: 'gemini-style', reasoningCapabilities: false, }, 'gemini-2.0-flash': { @@ -666,6 +668,7 @@ const geminiModelOptions = { // https://ai.google.dev/gemini-api/docs/pricing downloadable: false, supportsFIM: false, supportsSystemMessage: 'system-role', + specialToolFormat: 'gemini-style', reasoningCapabilities: false, }, 'gemini-2.0-flash-lite-preview-02-05': { @@ -675,6 +678,7 @@ const geminiModelOptions = { // https://ai.google.dev/gemini-api/docs/pricing downloadable: false, supportsFIM: false, supportsSystemMessage: 'system-role', + specialToolFormat: 'gemini-style', reasoningCapabilities: false, }, 'gemini-1.5-flash': { @@ -684,6 +688,7 @@ const geminiModelOptions = { // https://ai.google.dev/gemini-api/docs/pricing downloadable: false, supportsFIM: false, supportsSystemMessage: 'system-role', + specialToolFormat: 'gemini-style', reasoningCapabilities: false, }, 'gemini-1.5-pro': { @@ -693,6 +698,7 @@ const geminiModelOptions = { // https://ai.google.dev/gemini-api/docs/pricing downloadable: false, supportsFIM: false, supportsSystemMessage: 'system-role', + specialToolFormat: 'gemini-style', reasoningCapabilities: false, }, 'gemini-1.5-flash-8b': { @@ -702,6 +708,7 @@ const geminiModelOptions = { // https://ai.google.dev/gemini-api/docs/pricing downloadable: false, supportsFIM: false, supportsSystemMessage: 'system-role', + specialToolFormat: 'gemini-style', reasoningCapabilities: false, }, } as const satisfies { [s: string]: VoidStaticModelInfo } diff --git a/src/vs/workbench/contrib/void/common/prompt/prompts.ts b/src/vs/workbench/contrib/void/common/prompt/prompts.ts index f08f41bc..42f68636 100644 --- a/src/vs/workbench/contrib/void/common/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/common/prompt/prompts.ts @@ -212,8 +212,8 @@ export const voidTools description: `Returns full contents of a given file.`, params: { ...uriParam('file'), - start_line: { description: 'Optional. Do NOT fill this in unless you already know the line numbers you need to search. Defaults to 1.' }, - end_line: { description: 'Optional. Do NOT fill this in unless you already know the line numbers you need to search. Defaults to Infinity.' }, + start_line: { description: 'Optional. Do NOT fill this field in unless you were specifically given exact line numbers to search. Defaults to the beginning of the file.' }, + end_line: { description: 'Optional. Do NOT fill this field in unless you were specifically given exact line numbers to search. Defaults to the end of the file.' }, ...paginationParam, }, }, @@ -275,7 +275,7 @@ export const voidTools read_lint_errors: { name: 'read_lint_errors', - description: `Returns all lint errors on a given file.`, + description: `Use this tool to view all the lint errors on a file.`, params: { ...uriParam('file'), }, diff --git a/src/vs/workbench/contrib/void/common/sendLLMMessageTypes.ts b/src/vs/workbench/contrib/void/common/sendLLMMessageTypes.ts index 6f08096f..d76d6685 100644 --- a/src/vs/workbench/contrib/void/common/sendLLMMessageTypes.ts +++ b/src/vs/workbench/contrib/void/common/sendLLMMessageTypes.ts @@ -51,8 +51,22 @@ export type OpenAILLMChatMessage = { content: string; tool_call_id: string; } -export type LLMChatMessage = AnthropicLLMChatMessage | OpenAILLMChatMessage +export type GeminiLLMChatMessage = { + role: 'model' + parts: ( + | { text: string; } + | { functionCall: { name: ToolName, args: object } } + )[]; +} | { + role: 'user'; + parts: ( + | { text: string; } + | { functionResponse: { name: ToolName, response: { result: string } } } + )[]; +} + +export type LLMChatMessage = AnthropicLLMChatMessage | OpenAILLMChatMessage | GeminiLLMChatMessage diff --git a/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts b/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts index b6c466f4..d97372fa 100644 --- a/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts +++ b/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts @@ -57,7 +57,7 @@ export type ToolCallParams = { // RESULT OF TOOL CALL export type ToolResultType = { - 'read_file': { fileContents: string, totalFileLen: number, hasNextPage: boolean }, + 'read_file': { fileContents: string, totalFileLen: number, totalNumLines: number, hasNextPage: boolean }, 'ls_dir': { children: ShallowDirectoryItem[] | null, hasNextPage: boolean, hasPrevPage: boolean, itemsRemaining: number }, 'get_dir_tree': { str: string, }, 'search_pathnames_only': { uris: URI[], hasNextPage: boolean }, diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts index 4a515e50..fd01ac7d 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts @@ -10,6 +10,7 @@ import { Ollama } from 'ollama'; import OpenAI, { ClientOptions } from 'openai'; import { MistralCore } from '@mistralai/mistralai/core.js'; import { fimComplete } from '@mistralai/mistralai/funcs/fimComplete.js'; +import { GoogleGenerativeAI, Tool as GeminiTool, SchemaType, FunctionDeclaration, FunctionDeclarationSchemaProperty } from '@google/generative-ai'; // import { GoogleAuth } from 'google-auth-library' /* eslint-enable */ @@ -18,6 +19,7 @@ import { ChatMode, displayInfoOfProviderName, ModelSelectionOptions, ProviderNam 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'; +import { generateUuid } from '../../../../../base/common/uuid.js'; @@ -33,7 +35,11 @@ type InternalCommonMessageParams = { _setAborter: (aborter: () => void) => void; } -type SendChatParams_Internal = InternalCommonMessageParams & { messages: LLMChatMessage[]; separateSystemMessage: string | undefined; chatMode: ChatMode | null; } +type SendChatParams_Internal = InternalCommonMessageParams & { + messages: LLMChatMessage[]; + separateSystemMessage: string | undefined; + chatMode: ChatMode | null; +} type SendFIMParams_Internal = InternalCommonMessageParams & { messages: LLMFIMMessage; separateSystemMessage: string | undefined; } export type ListParams_Internal = ModelListParams @@ -42,13 +48,6 @@ const invalidApiKeyMessage = (providerName: ProviderName) => `Invalid ${displayI // ------------ OPENAI-COMPATIBLE (HELPERS) ------------ -// const getGoogleApiKey = async () => { -// // module‑level singleton -// const auth = new GoogleAuth({ scopes: `https://www.googleapis.com/auth/cloud-platform` }); -// const key = await auth.getAccessToken() -// if (!key) throw new Error(`Google API failed to generate a key.`) -// return key -// } const newOpenAICompatibleSDK = async ({ settingsOfProvider, providerName, includeInPayload }: { settingsOfProvider: SettingsOfProvider, providerName: ProviderName, includeInPayload?: { [s: string]: any } }) => { @@ -88,10 +87,6 @@ const newOpenAICompatibleSDK = async ({ settingsOfProvider, providerName, includ ...commonPayloadOpts, }) } - else if (providerName === 'gemini') { - const thisConfig = settingsOfProvider[providerName] - return new OpenAI({ baseURL: 'https://generativelanguage.googleapis.com/v1beta/openai', apiKey: thisConfig.apiKey, ...commonPayloadOpts }) - } // else if (providerName === 'googleVertex') { // // https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/call-vertex-using-openai-library // const thisConfig = settingsOfProvider[providerName] @@ -131,6 +126,7 @@ const newOpenAICompatibleSDK = async ({ settingsOfProvider, providerName, includ const _sendOpenAICompatibleFIM = async ({ messages: { prefix, suffix, stopTokens }, onFinalMessage, onError, settingsOfProvider, modelName: modelName_, _setAborter, providerName, }: SendFIMParams_Internal) => { + const { modelName, supportsFIM } = getModelCapabilities(providerName, modelName_) if (!supportsFIM) { if (modelName === modelName_) @@ -193,7 +189,7 @@ const openAITools = (chatMode: ChatMode) => { return openAITools } -const openAIToolToRawToolCallObj = (name: string, toolParamsStr: string, id: string): RawToolCallObj | null => { +const rawToolCallObjOf = (name: string, toolParamsStr: string, id: string): RawToolCallObj | null => { if (!isAToolName(name)) return null const rawParams: RawToolParamsObj = {} let input: unknown @@ -297,6 +293,7 @@ const _sendOpenAICompatibleChat = async ({ messages, onText, onFinalMessage, onE fullReasoningSoFar += newReasoning } + // call onText onText({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar, @@ -309,7 +306,7 @@ const _sendOpenAICompatibleChat = async ({ messages, onText, onFinalMessage, onE onError({ message: 'Void: Response from model was empty.', fullError: null }) } else { - const toolCall = openAIToolToRawToolCallObj(toolName, toolParamsStr, toolId) + const toolCall = rawToolCallObjOf(toolName, toolParamsStr, toolId) const toolCallObj = toolCall ? { toolCall } : {} onFinalMessage({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar, anthropicReasoning: null, ...toolCallObj }); } @@ -438,7 +435,7 @@ const sendAnthropicChat = async ({ messages, providerName, onText, onFinalMessag }) - // manually parse out tool results + // manually parse out tool results if XML if (!specialToolFormat) { const { newOnText, newOnFinalMessage } = extractXMLToolsWrapper(onText, onFinalMessage, chatMode) onText = newOnText @@ -544,6 +541,7 @@ const sendMistralFIM = ({ messages, onFinalMessage, onError, settingsOfProvider, stop: messages.stopTokens, }) .then(async response => { + // unfortunately, _setAborter() does not exist let content = response?.ok ? response.value.choices?.[0]?.message?.content ?? '' : ''; const fullText = typeof content === 'string' ? content @@ -620,6 +618,165 @@ const sendOllamaFIM = ({ messages, onFinalMessage, onError, settingsOfProvider, }) } +// ---------------- GEMINI NATIVE IMPLEMENTATION ---------------- + + + +const toGeminiFunctionDecl = (toolInfo: InternalToolInfo) => { + const { name, description, params } = toolInfo + const paramsWithType: { [k: string]: FunctionDeclarationSchemaProperty } = {} + for (const key in params) { + paramsWithType[key] = { type: SchemaType.STRING, ...params[key] } + } + return { + name, + description, + parameters: { + type: SchemaType.OBJECT, + properties: paramsWithType, + } + } satisfies FunctionDeclaration +} + + +const geminiTools = (chatMode: ChatMode): GeminiTool[] | null => { + const allowedTools = availableTools(chatMode) + if (!allowedTools || Object.keys(allowedTools).length === 0) return null + const functionDecls: FunctionDeclaration[] = [] + for (const t in allowedTools ?? {}) { + functionDecls.push(toGeminiFunctionDecl(allowedTools[t])) + } + const tools: GeminiTool = { functionDeclarations: functionDecls, } + return [tools] +} + + + +// Implementation for Gemini using Google's native API +const sendGeminiChat = async ({ + messages, + separateSystemMessage, + onText, + onFinalMessage, + onError, + settingsOfProvider, + modelName: modelName_, + _setAborter, + providerName, + modelSelectionOptions, + chatMode, +}: SendChatParams_Internal) => { + + console.log('MESSAGES', JSON.stringify(messages, null, 2)) + + if (providerName !== 'gemini') throw new Error(`Sending Gemini chat, but provider was ${providerName}`) + + const thisConfig = settingsOfProvider[providerName] + + const { + modelName, + specialToolFormat, + // reasoningCapabilities, + } = getModelCapabilities(providerName, modelName_) + + const { providerReasoningIOSettings } = getProviderCapabilities(providerName) + + // reasoning + // const { canIOReasoning, openSourceThinkTags, } = reasoningCapabilities || {} + const reasoningInfo = getSendableReasoningInfo('Chat', providerName, modelName_, modelSelectionOptions) // user's modelName_ here + const includeInPayload = providerReasoningIOSettings?.input?.includeInPayload?.(reasoningInfo) || {} + + // tools + const potentialTools = chatMode !== null ? geminiTools(chatMode) : null + const nativeToolsObj = potentialTools && specialToolFormat === 'gemini-style' ? + { tools: potentialTools } as const + : {} + + // instance + const genAI = new GoogleGenerativeAI( + thisConfig.apiKey + ); + const model = genAI.getGenerativeModel({ + systemInstruction: separateSystemMessage, + model: modelName, + }); + + // manually parse out tool results if XML + if (!specialToolFormat) { + const { newOnText, newOnFinalMessage } = extractXMLToolsWrapper(onText, onFinalMessage, chatMode) + onText = newOnText + onFinalMessage = newOnFinalMessage + } + + // when receive text + let fullReasoningSoFar = '' + let fullTextSoFar = '' + + let toolName = '' + let toolParamsStr = '' + + model.generateContentStream({ + systemInstruction: separateSystemMessage ?? undefined, + contents: messages as any, + ...includeInPayload, + ...nativeToolsObj, + }) + .then(async ({ stream, response }) => { + _setAborter(() => { stream.return(fullTextSoFar); }); + + // Process the stream + for await (const chunk of stream) { + // message + const newText = chunk.text() ?? '' + fullTextSoFar += newText + + // tool call + const functionCalls = chunk.functionCalls() + if (functionCalls && functionCalls.length > 0) { + const functionCall = functionCalls[0] // Get the first function call + toolName = functionCall.name ?? '' + toolParamsStr = JSON.stringify(functionCall.args ?? {}) + } + + // (do not handle reasoning yet) + + // call onText + onText({ + fullText: fullTextSoFar, + fullReasoning: fullReasoningSoFar, + toolCall: isAToolName(toolName) ? { name: toolName, rawParams: {}, isDone: false, doneParams: [], id: 'dummy' } : undefined, + }) + } + + // on final + if (!fullTextSoFar && !fullReasoningSoFar && !toolName) { + onError({ message: 'Void: Response from model was empty.', fullError: null }) + } else { + const toolId = generateUuid() // gemini does not generate tool IDs. Generate one + const toolCall = rawToolCallObjOf(toolName, toolParamsStr, toolId) + const toolCallObj = toolCall ? { toolCall } : {} + onFinalMessage({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar, anthropicReasoning: null, ...toolCallObj }); + } + }) + .catch(error => { + const message = error?.message + if (typeof message === 'string') { + + if (error.message?.includes('API key')) { + onError({ message: invalidApiKeyMessage(providerName), fullError: error }); + } + else if (error?.message?.includes('429')) { + onError({ message: 'Rate limit reached. ' + error, fullError: error }); + } + else + onError({ message: error + '', fullError: error }); + } + else { + onError({ message: error + '', fullError: error }); + } + }) +}; + type CallFnOfProvider = { @@ -647,7 +804,7 @@ export const sendLLMMessageToProviderImplementation = { list: null, }, gemini: { - sendChat: (params) => _sendOpenAICompatibleChat(params), + sendChat: (params) => sendGeminiChat(params), sendFIM: null, list: null, }, diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts index f19c9b1b..e1c18f50 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts @@ -33,13 +33,6 @@ export const sendLLMMessage = async ({ // only captures number of messages and message "shape", no actual code, instructions, prompts, etc const captureLLMEvent = (eventId: string, extras?: object) => { - let totalTokens = 0 - if (messagesType === 'chatMessages') { - for (const m of messages_) totalTokens += m.content.length - } - else { - totalTokens = messages_.prefix.length + messages_.suffix.length - } metricsService.capture(eventId, { providerName, @@ -48,13 +41,10 @@ export const sendLLMMessage = async ({ numModelsAtEndpoint: settingsOfProvider[providerName]?.models?.length, ...messagesType === 'chatMessages' ? { numMessages: messages_?.length, - messagesShape: messages_?.map(msg => ({ role: msg.role, length: msg.content.length })), - } : messagesType === 'FIMMessage' ? { prefixLength: messages_.prefix.length, suffixLength: messages_.suffix.length, } : {}, - totalTokens, ...loggingExtras, ...extras, }) @@ -103,7 +93,7 @@ export const sendLLMMessage = async ({ if (messagesType === 'chatMessages') - captureLLMEvent(`${loggingName} - Sending Message`, { userMessageLength: messages_?.[messages_.length - 1]?.content.length }) + captureLLMEvent(`${loggingName} - Sending Message`, {}) else if (messagesType === 'FIMMessage') captureLLMEvent(`${loggingName} - Sending FIM`, { prefixLen: messages_?.prefix?.length, suffixLen: messages_?.suffix?.length })