native gemini tools, editing UI, tool prompts

This commit is contained in:
Andrew Pareles 2025-04-30 20:27:03 -07:00
parent cec0df989c
commit f166fa3581
13 changed files with 528 additions and 286 deletions

8
package-lock.json generated
View file

@ -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"

View file

@ -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",

View file

@ -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<IConvertToLLMMessageService>('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 };
}

View file

@ -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 => {

View file

@ -832,55 +832,39 @@ const EditTool = ({ toolMessage, threadId, messageIdx, content }: Parameters<Res
}
else if (toolMessage.type === 'success' || toolMessage.type === 'rejected' || toolMessage.type === 'tool_error') {
// add apply box
if (params) {
const applyBoxId = getApplyBoxId({
threadId: threadId,
messageIdx: messageIdx,
tokenIdx: 'N/A',
})
componentParams.desc2 = <EditToolHeaderButtons
applyBoxId={applyBoxId}
uri={params.uri}
codeStr={content}
/>
}
const applyBoxId = getApplyBoxId({
threadId: threadId,
messageIdx: messageIdx,
tokenIdx: 'N/A',
})
componentParams.desc2 = <EditToolHeaderButtons
applyBoxId={applyBoxId}
uri={params.uri}
codeStr={content}
/>
// add children
componentParams.children = <ToolChildrenWrapper className='bg-void-bg-3'>
<EditToolChildren
uri={params.uri}
code={content}
/>
</ToolChildrenWrapper>
if (toolMessage.type !== 'tool_error') {
const { result } = toolMessage
componentParams.bottomChildren = <EditToolLintErrors lintErrors={result?.lintErrors || []} />
componentParams.children = <ToolChildrenWrapper className='bg-void-bg-3'>
<EditToolChildren
uri={params.uri}
code={content}
/>
</ToolChildrenWrapper>
componentParams.bottomChildren = <BottomChildren title='Lint errors'>
{result?.lintErrors?.map((error, i) => (
<div key={i} className="">Lines {error.startLineNumber}-{error.endLineNumber}: {error.message}</div>
))}
</BottomChildren>
}
else {
// error
const { result } = toolMessage
if (params) {
componentParams.children = <ToolChildrenWrapper className='bg-void-bg-3'>
{/* error */}
<CodeChildren>
{result}
</CodeChildren>
{/* content */}
<EditToolChildren
uri={params.uri}
code={content}
/>
</ToolChildrenWrapper>
}
else {
componentParams.children = <CodeChildren>
{result}
</CodeChildren>
}
componentParams.bottomChildren = <BottomChildren title='Error'>
{result}
</BottomChildren>
}
}
@ -1063,87 +1047,87 @@ const UserMessageComponent = ({ chatMessage, messageIdx, isCheckpointGhost, curr
setSelections={setStagingSelections}
>
<VoidInputBox2
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}
/>
</VoidChatArea>
}
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}
/>
</VoidChatArea>
}
const isMsgAfterCheckpoint = currCheckpointIdx !== undefined && currCheckpointIdx === messageIdx - 1
const isMsgAfterCheckpoint = currCheckpointIdx !== undefined && currCheckpointIdx === messageIdx - 1
return <div
// align chatbubble accoridng to role
className={`
return <div
// align chatbubble accoridng to role
className={`
relative ml-auto
${mode === 'edit' ? 'w-full max-w-full'
: mode === 'display' ? `self-end w-fit max-w-full whitespace-pre-wrap` : '' // user words should be pre
}
: mode === 'display' ? `self-end w-fit max-w-full whitespace-pre-wrap` : '' // user words should be pre
}
${isCheckpointGhost && !isMsgAfterCheckpoint ? 'opacity-50 pointer-events-none' : ''}
`}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<div
// style chatbubble according to role
className={`
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<div
// style chatbubble according to role
className={`
text-left rounded-lg max-w-full
${mode === 'edit' ? ''
: mode === 'display' ? 'p-2 flex flex-col bg-void-bg-1 text-void-fg-1 overflow-x-auto cursor-pointer' : ''
}
: mode === 'display' ? 'p-2 flex flex-col bg-void-bg-1 text-void-fg-1 overflow-x-auto cursor-pointer' : ''
}
`}
onClick={() => { if (mode === 'display') { onOpenEdit() } }}
>
{chatbubbleContents}
</div>
onClick={() => { if (mode === 'display') { onOpenEdit() } }}
>
{chatbubbleContents}
</div>
<div
className="absolute -top-1 -right-1 translate-x-0 -translate-y-0 z-1"
// data-tooltip-id='void-tooltip'
// data-tooltip-content='Edit message'
// data-tooltip-place='left'
>
<EditSymbol
size={18}
className={`
<div
className="absolute -top-1 -right-1 translate-x-0 -translate-y-0 z-1"
// data-tooltip-id='void-tooltip'
// data-tooltip-content='Edit message'
// data-tooltip-place='left'
>
<EditSymbol
size={18}
className={`
cursor-pointer
p-[2px]
bg-void-bg-1 border border-void-border-1 rounded-md
transition-opacity duration-200 ease-in-out
${isHovered || (isFocused && mode === 'edit') ? 'opacity-100' : 'opacity-0'}
`}
onClick={() => {
if (mode === 'display') {
onOpenEdit()
} else if (mode === 'edit') {
onCloseEdit()
}
}}
/>
</div>
onClick={() => {
if (mode === 'display') {
onOpenEdit()
} else if (mode === 'edit') {
onCloseEdit()
}
}}
/>
</div>
</div>
</div>
}
const SmallProseWrapper = ({ children }: { children: React.ReactNode }) => {
return <div className='
return <div className='
text-void-fg-4
prose
prose-sm
@ -1200,12 +1184,12 @@ prose-pre:my-2
prose-table:text-[13px]
'>
{children}
</div>
{children}
</div>
}
const ProseWrapper = ({ children }: { children: React.ReactNode }) => {
return <div className='
return <div className='
text-void-fg-2
prose
prose-sm
@ -1230,77 +1214,77 @@ prose-ul:leading-normal
max-w-none
'
>
{children}
</div>
>
{children}
</div>
}
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 &&
<div className={`${isCheckpointGhost ? 'opacity-50' : ''}`}>
<ReasoningWrapper isDoneReasoning={isDoneReasoning} isStreaming={!isCommitted}>
<SmallProseWrapper>
<ChatMarkdownRender
string={reasoningStr}
chatMessageLocation={chatMessageLocation}
isApplyEnabled={false}
isLinkDetectionEnabled={true}
/>
</SmallProseWrapper>
</ReasoningWrapper>
</div>
}
return <>
{/* reasoning token */}
{hasReasoning &&
<div className={`${isCheckpointGhost ? 'opacity-50' : ''}`}>
<ReasoningWrapper isDoneReasoning={isDoneReasoning} isStreaming={!isCommitted}>
<SmallProseWrapper>
<ChatMarkdownRender
string={reasoningStr}
chatMessageLocation={chatMessageLocation}
isApplyEnabled={false}
isLinkDetectionEnabled={true}
/>
</SmallProseWrapper>
</ReasoningWrapper>
</div>
}
{/* assistant message */}
{chatMessage.displayContent &&
<div className={`${isCheckpointGhost ? 'opacity-50' : ''}`}>
<ProseWrapper>
<ChatMarkdownRender
string={chatMessage.displayContent || ''}
chatMessageLocation={chatMessageLocation}
isApplyEnabled={true}
isLinkDetectionEnabled={true}
/>
</ProseWrapper>
</div>
}
</>
{/* assistant message */}
{chatMessage.displayContent &&
<div className={`${isCheckpointGhost ? 'opacity-50' : ''}`}>
<ProseWrapper>
<ChatMarkdownRender
string={chatMessage.displayContent || ''}
chatMessageLocation={chatMessageLocation}
isApplyEnabled={true}
isLinkDetectionEnabled={true}
/>
</ProseWrapper>
</div>
}
</>
}
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 <ToolHeaderWrapper title='Reasoning' desc1={isWriting ? <IconLoading /> : ''} isOpen={isOpen} onClick={() => setIsOpen(v => !v)}>
<ToolChildrenWrapper>
<div className='!select-text cursor-auto'>
{children}
</div>
</ToolChildrenWrapper>
</ToolHeaderWrapper>
const isDone = isDoneReasoning || !isStreaming
const isWriting = !isDone
const [isOpen, setIsOpen] = useState(isWriting)
useEffect(() => {
if (!isWriting) setIsOpen(false) // if just finished reasoning, close
}, [isWriting])
return <ToolHeaderWrapper title='Reasoning' desc1={isWriting ? <IconLoading /> : ''} isOpen={isOpen} onClick={() => setIsOpen(v => !v)}>
<ToolChildrenWrapper>
<div className='!select-text cursor-auto'>
{children}
</div>
</ToolChildrenWrapper>
</ToolHeaderWrapper>
}
@ -1309,10 +1293,10 @@ return <ToolHeaderWrapper title='Reasoning' desc1={isWriting ? <IconLoading /> :
// 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 <span className='flex items-center flex-nowrap'>
{item}
<IconLoading className='w-3 text-sm' />
</span>
return <span className='flex items-center flex-nowrap'>
{item}
<IconLoading className='w-3 text-sm' />
</span>
}
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[] }) => {
</div>
}
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 (
<div className="w-full px-2 mt-0.5">
@ -1590,15 +1574,13 @@ const EditToolLintErrors = ({ lintErrors }: { lintErrors: LintErrorItem[] }) =>
<ChevronRight
className={`mr-1 h-3 w-3 flex-shrink-0 transition-transform duration-100 text-void-fg-4 group-hover:text-void-fg-3 ${isOpen ? 'rotate-90' : ''}`}
/>
<span className="font-medium text-void-fg-4 group-hover:text-void-fg-3 text-xs">Lint errors</span>
<span className="font-medium text-void-fg-4 group-hover:text-void-fg-3 text-xs">{title}</span>
</div>
<div
className={`overflow-hidden transition-all duration-200 ease-in-out ${isOpen ? 'opacity-100' : 'max-h-0 opacity-0'} text-xs pl-4`}
>
<div className="flex flex-col gap-0.5 overflow-x-auto whitespace-nowrap text-void-fg-4 opacity-90 border-l-2 border-void-warning px-2 py-0.5">
{lintErrors.map((error, i) => (
<div key={i} className="">Lines {error.startLineNumber}-{error.endLineNumber}: {error.message}</div>
))}
{children}
</div>
</div>
</div>
@ -2178,7 +2160,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
return <ToolHeaderWrapper {...componentParams} />
}
},
'rewrite_file': {
'rewrite_file': {
resultWrapper: (params) => {
return <EditTool {...params} content={`${'```\n'}${params.toolMessage.params.newContent}${'\n```'}`} />
}
@ -2202,7 +2184,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
return <CommandTool {...params} type='run_persistent_command' />
}
},
'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<HTMLDivElement>(null)
const scrollContainerRef = useRef<HTMLDivElement | null>(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)
}

View file

@ -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.`)
},

View file

@ -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);

View file

@ -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 }

View file

@ -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'),
},

View file

@ -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

View file

@ -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 },

View file

@ -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<ModelResponse> = ModelListParams<ModelResponse>
@ -42,13 +48,6 @@ const invalidApiKeyMessage = (providerName: ProviderName) => `Invalid ${displayI
// ------------ OPENAI-COMPATIBLE (HELPERS) ------------
// const getGoogleApiKey = async () => {
// // modulelevel 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,
},

View file

@ -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 })