Merge pull request #417 from voideditor/model-selection

Misc updates
This commit is contained in:
Andrew Pareles 2025-04-18 20:58:55 -07:00 committed by GitHub
commit c775d34d75
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 1401 additions and 1891 deletions

1589
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -79,6 +79,7 @@
"@mistralai/mistralai": "^1.5.0",
"@modelcontextprotocol/sdk": "^1.9.0",
"@parcel/watcher": "2.5.1",
"@types/katex": "^0.16.7",
"@types/semver": "^7.5.8",
"@vscode/deviceid": "^0.1.1",
"@vscode/iconv-lite-umd": "0.7.0",
@ -106,10 +107,13 @@
"cross-spawn": "^7.0.6",
"diff": "^7.0.0",
"eslint-plugin-react": "^7.37.4",
"fast-json-stable-stringify": "^2.1.0",
"google-auth-library": "^9.15.1",
"groq-sdk": "^0.15.0",
"http-proxy-agent": "^7.0.0",
"https-proxy-agent": "^7.0.2",
"jschardet": "3.1.4",
"katex": "^0.16.22",
"kerberos": "2.1.1",
"lucide-react": "^0.477.0",
"marked": "^15.0.7",

View file

@ -1,7 +1,7 @@
{
"nameShort": "Void",
"nameLong": "Void",
"voidVersion": "1.2.5",
"voidVersion": "1.2.6",
"applicationName": "void",
"dataFolderName": ".void-editor",
"win32MutexName": "voideditor",

View file

@ -59,6 +59,6 @@ class DummyService extends Disposable implements IWorkbenchContribution, IDummyS
// pick one and delete the other:
registerSingleton(IDummyService, DummyService, InstantiationType.Eager);
registerSingleton(IDummyService, DummyService, InstantiationType.Eager); // lazily loaded, even if Eager
registerWorkbenchContribution2(DummyService.ID, DummyService, WorkbenchPhase.BlockRestore);
registerWorkbenchContribution2(DummyService.ID, DummyService, WorkbenchPhase.BlockRestore); // mounts on start

View file

@ -32,6 +32,9 @@ import { INotificationService, Severity } from '../../../../platform/notificatio
import { truncate } from '../../../../base/common/strings.js';
import { THREAD_STORAGE_KEY } from '../common/storageKeys.js';
import { IConvertToLLMMessageService } from './convertToLLMMessageService.js';
import { timeout } from '../../../../base/common/async.js';
const CHAT_RETRIES = 3
export const findStagingSelectionIndex = (currentSelections: StagingSelectionItem[] | undefined, newSelection: StagingSelectionItem): number | null => {
if (!currentSelections) return null
@ -91,7 +94,7 @@ const defaultMessageState: UserMessageState = {
// a 'thread' means a chat message history
type ThreadType = {
export type ThreadType = {
id: string; // store the id here too
createdAt: string; // ISO string
lastModified: string; // ISO string
@ -177,6 +180,7 @@ export interface IChatThreadService {
getCurrentThread(): ThreadType;
openNewThread(): void;
deleteThread(threadId: string): void;
switchToThread(threadId: string): void;
// exposed getters/setters
@ -564,7 +568,6 @@ class ChatThreadService extends Disposable implements IChatThreadService {
let nMessagesSent = 0
let shouldSendAnotherMessage = true
let isRunningWhenEnd: IsRunningType = undefined
let aborted = false
// before enter loop, call tool
if (callThisToolFirst) {
@ -592,69 +595,94 @@ class ChatThreadService extends Disposable implements IChatThreadService {
chatMode
})
const llmCancelToken = this._llmMessageService.sendLLMMessage({
messagesType: 'chatMessages',
chatMode,
messages: messages,
modelSelection,
modelSelectionOptions,
logging: { loggingName: `Chat - ${chatMode}`, loggingExtras: { threadId, nMessagesSent, chatMode } },
separateSystemMessage: separateSystemMessage,
onText: ({ fullText, fullReasoning, toolCall }) => {
this._setStreamState(threadId, { displayContentSoFar: fullText, reasoningSoFar: fullReasoning, toolCallSoFar: toolCall }, 'merge')
},
onFinalMessage: async ({ fullText, fullReasoning, toolCall, anthropicReasoning, }) => {
this._addMessageToThread(threadId, { role: 'assistant', displayContent: fullText, reasoning: fullReasoning, anthropicReasoning })
this._setStreamState(threadId, { displayContentSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined, toolCallSoFar: undefined }, 'merge')
resMessageIsDonePromise(toolCall) // resolve with tool calls
},
onError: (error) => {
const messageSoFar = this.streamState[threadId]?.displayContentSoFar ?? ''
const reasoningSoFar = this.streamState[threadId]?.reasoningSoFar ?? ''
// const toolCallSoFar = this.streamState[threadId]?.toolCallSoFar
// add assistant's message to chat history, and clear selection
this._addMessageToThread(threadId, { role: 'assistant', displayContent: messageSoFar, reasoning: reasoningSoFar, anthropicReasoning: null })
this._setStreamState(threadId, { error }, 'set')
resMessageIsDonePromise()
},
onAbort: () => {
// stop the loop to free up the promise, but don't modify state (already handled by whatever stopped it)
resMessageIsDonePromise()
this._metricsService.capture('Agent Loop Done (Aborted)', { nMessagesSent, chatMode })
aborted = true
},
})
// should never happen, just for safety
if (llmCancelToken === null) {
this._setStreamState(threadId, {
error: { message: 'There was an unexpected error when sending your chat message.', fullError: null }
}, 'set')
break
}
this._setStreamState(threadId, { streamingToken: llmCancelToken }, 'merge') // new stream token for the new message
const toolCall = await messageIsDonePromise // wait for message to complete
if (aborted) { return }
this._setStreamState(threadId, { streamingToken: undefined }, 'merge') // streaming message is done
let aborted = false
// call tool if there is one
const tool: RawToolCallObj | undefined = toolCall
if (tool) {
const { awaitingUserApproval, interrupted } = await this._runToolCall(threadId, tool.name, tool.id, { preapproved: false, unvalidatedToolParams: tool.rawParams })
let shouldRetry = true
let nAttempts = 0
// stop if interrupted. we don't have to do this for llmMessage because we have a stream token for it and onAbort gets called, but we don't have the equivalent for tools.
// just detect tool interruption which is the same as chat interruption right now
if (interrupted) { return }
while (shouldRetry) {
shouldRetry = false
if (awaitingUserApproval) {
isRunningWhenEnd = 'awaiting_user'
const llmCancelToken = this._llmMessageService.sendLLMMessage({
messagesType: 'chatMessages',
chatMode,
messages: messages,
modelSelection,
modelSelectionOptions,
logging: { loggingName: `Chat - ${chatMode}`, loggingExtras: { threadId, nMessagesSent, chatMode } },
separateSystemMessage: separateSystemMessage,
onText: ({ fullText, fullReasoning, toolCall }) => {
this._setStreamState(threadId, { displayContentSoFar: fullText, reasoningSoFar: fullReasoning, toolCallSoFar: toolCall }, 'merge')
},
onFinalMessage: async ({ fullText, fullReasoning, toolCall, anthropicReasoning, }) => {
this._addMessageToThread(threadId, { role: 'assistant', displayContent: fullText, reasoning: fullReasoning, anthropicReasoning })
this._setStreamState(threadId, { displayContentSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined, toolCallSoFar: undefined }, 'merge')
resMessageIsDonePromise(toolCall) // resolve with tool calls
},
onError: (error) => {
const messageSoFar = this.streamState[threadId]?.displayContentSoFar ?? ''
const reasoningSoFar = this.streamState[threadId]?.reasoningSoFar ?? ''
if (nAttempts < CHAT_RETRIES) {
nAttempts += 1
shouldRetry = true
this._setStreamState(threadId, { displayContentSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined, toolCallSoFar: undefined }, 'merge')
timeout(2500).then(() => { resMessageIsDonePromise() })
}
else {
// const toolCallSoFar = this.streamState[threadId]?.toolCallSoFar
// add assistant's message to chat history, and clear selection
this._addMessageToThread(threadId, { role: 'assistant', displayContent: messageSoFar, reasoning: reasoningSoFar, anthropicReasoning: null })
this._setStreamState(threadId, { error }, 'set')
resMessageIsDonePromise()
}
},
onAbort: () => {
// stop the loop to free up the promise, but don't modify state (already handled by whatever stopped it)
resMessageIsDonePromise()
this._metricsService.capture('Agent Loop Done (Aborted)', { nMessagesSent, chatMode })
aborted = true
},
})
// should never happen, just for safety
if (llmCancelToken === null) {
this._setStreamState(threadId, {
error: { message: 'There was an unexpected error when sending your chat message.', fullError: null }
}, 'set')
break
}
else {
shouldSendAnotherMessage = true
this._setStreamState(threadId, { streamingToken: llmCancelToken }, 'merge') // new stream token for the new message
const toolCall = await messageIsDonePromise // wait for message to complete
if (shouldRetry) {
continue
}
}
if (aborted) {
return
}
this._setStreamState(threadId, { streamingToken: undefined }, 'merge') // streaming message is done
} // end while
// call tool if there is one
const tool: RawToolCallObj | undefined = toolCall
if (tool) {
const { awaitingUserApproval, interrupted } = await this._runToolCall(threadId, tool.name, tool.id, { preapproved: false, unvalidatedToolParams: tool.rawParams })
// stop if interrupted. we don't have to do this for llmMessage because we have a stream token for it and onAbort gets called, but we don't have the equivalent for tools.
// just detect tool interruption which is the same as chat interruption right now
if (interrupted) { return }
if (awaitingUserApproval) {
isRunningWhenEnd = 'awaiting_user'
}
else {
shouldSendAnotherMessage = true
}
}
} // end while (attempts)
} // end while (send message)
// if awaiting user approval, keep isRunning true, else end isRunning
@ -1389,6 +1417,19 @@ We only need to do it for files that were edited since `from`, ie files between
}
deleteThread(threadId: string): void {
const { allThreads: currentThreads } = this.state
// delete the thread
const newThreads = { ...currentThreads };
delete newThreads[threadId];
// store the updated threads
this._storeAllThreads(newThreads);
this._setState({ ...this.state, allThreads: newThreads }, true)
}
private _addMessageToThread(threadId: string, message: ChatMessage) {
const { allThreads } = this.state
const oldThread = allThreads[threadId]

View file

@ -438,27 +438,33 @@ class ConvertToLLMMessageService extends Disposable implements IConvertToLLMMess
super()
}
// Read .voidinstructions files from workspace folders
private _getVoidInstructionsFileContents(): string {
const workspaceFolders = this.workspaceContextService.getWorkspace().folders;
let voidInstructions = '';
for (const folder of workspaceFolders) {
const uri = URI.joinPath(folder.uri, '.voidinstructions')
const { model } = this.voidModelService.getModel(uri)
if (!model) continue
voidInstructions += model.getValue() + '\n\n';
// Read .voidrules files from workspace folders
private _getVoidRulesFileContents(): string {
try {
const workspaceFolders = this.workspaceContextService.getWorkspace().folders;
let voidRules = '';
for (const folder of workspaceFolders) {
const uri = URI.joinPath(folder.uri, '.voidrules')
const { model } = this.voidModelService.getModel(uri)
if (!model) continue
voidRules += model.getValue() + '\n\n';
}
return voidRules.trim();
}
catch (e) {
console.log('Could not read .voidrules, continuing...')
return ''
}
return voidInstructions.trim();
}
// Get combined AI instructions from settings and .voidinstructions files
// Get combined AI instructions from settings and .voidrules files
private _getCombinedAIInstructions(): string {
const globalAIInstructions = this.voidSettingsService.state.globalSettings.aiInstructions;
const voidInstructionsFileContent = this._getVoidInstructionsFileContents();
const voidRulesFileContent = this._getVoidRulesFileContents();
const ans: string[] = []
if (globalAIInstructions) ans.push(globalAIInstructions)
if (voidInstructionsFileContent) ans.push(voidInstructionsFileContent)
if (voidRulesFileContent) ans.push(voidRulesFileContent)
return ans.join('\n\n')
}

View file

@ -21,8 +21,8 @@ class ConvertContribWorkbenchContribution extends Disposable implements IWorkben
const initializeURI = (uri: URI) => {
this.workspaceContext.getWorkspace()
const voidInstrsURI = URI.joinPath(uri, '.voidinstructions')
this.voidModelService.initializeModel(voidInstrsURI)
const voidRulesURI = URI.joinPath(uri, '.voidrules')
this.voidModelService.initializeModel(voidRulesURI)
}
// call

View file

@ -327,7 +327,7 @@ class DirectoryStrService extends Disposable implements IDirectoryStrService {
async getDirectoryStrTool(uri: URI, options?: { maxItemsPerDir?: number }) {
const eRoot = this.explorerService.findClosest(uri)
if (!eRoot) throw new Error(`There was a problem reading the URI: ${uri.fsPath}.`)
if (!eRoot) throw new Error(`The folder ${uri.fsPath} does not exist.`)
const maxItemsPerDir = options?.maxItemsPerDir ?? START_MAX_ITEMS_PER_DIR; // Use START_MAX_ITEMS_PER_DIR

View file

@ -37,7 +37,7 @@ export const IconShell1 = ({ onClick, Icon, disabled, className, ...props }: Ico
size-[18px]
p-[2px]
flex items-center justify-center
text-sm bg-void-bg-3 text-void-fg-3
text-sm text-void-fg-3
hover:brightness-110
disabled:opacity-50 disabled:cursor-not-allowed
${className}

View file

@ -5,6 +5,9 @@
import React, { JSX, useMemo, useState } from 'react'
import { marked, MarkedToken, Token } from 'marked'
import katex from 'katex'
import 'katex/dist/katex.min.css'
import dompurify from '../../../../../../../base/browser/dompurify/dompurify.js'
import { convertToVscodeLang, detectLanguage } from '../../../../common/helpers/languageHelpers.js'
import { BlockCodeApplyWrapper } from './ApplyBlockHoverButtons.js'
@ -31,6 +34,63 @@ function isValidUri(s: string): boolean {
return s.length > 5 && isAbsolute(s) && !s.includes('//') && !s.includes('/*') // common case that is a false positive is comments like //
}
// renders contiguous string of latex eg $e^{i\pi}$
const LatexRender = ({ latex }: { latex: string }) => {
try {
let formula = latex;
let displayMode = false;
// Extract the formula from delimiters
if (latex.startsWith('$') && latex.endsWith('$')) {
// Check if it's display math $$...$$
if (latex.startsWith('$$') && latex.endsWith('$$')) {
formula = latex.slice(2, -2);
displayMode = true;
} else {
formula = latex.slice(1, -1);
}
} else if (latex.startsWith('\\(') && latex.endsWith('\\)')) {
formula = latex.slice(2, -2);
} else if (latex.startsWith('\\[') && latex.endsWith('\\]')) {
formula = latex.slice(2, -2);
displayMode = true;
}
// Render LaTeX
const html = katex.renderToString(formula, {
displayMode: displayMode,
throwOnError: false,
output: 'html'
});
// Sanitize the HTML output with DOMPurify
const sanitizedHtml = dompurify.sanitize(html, {
RETURN_TRUSTED_TYPE: true,
USE_PROFILES: { html: true, svg: true, mathMl: true }
});
// Add proper styling based on mode
const className = displayMode
? 'katex-block my-2 text-center'
: 'katex-inline';
// Use the ref approach to avoid dangerouslySetInnerHTML
const mathRef = React.useRef<HTMLSpanElement>(null);
React.useEffect(() => {
if (mathRef.current) {
mathRef.current.innerHTML = sanitizedHtml as unknown as string;
}
}, [sanitizedHtml]);
return <span ref={mathRef} className={className}></span>;
} catch (error) {
console.error('KaTeX rendering error:', error);
return <span className="katex-error text-red-500">{latex}</span>;
}
}
const Codespan = ({ text, className, onClick }: { text: string, className?: string, onClick?: () => void }) => {
// TODO compute this once for efficiency. we should use `labels.ts/shorten` to display duplicates properly
@ -108,6 +168,105 @@ const CodespanWithLink = ({ text, rawText, chatMessageLocation }: { text: string
}
const paragraphToLatexSegments = (paragraphText: string) => {
const segments: React.ReactNode[] = [];
if (paragraphText
&& !(paragraphText.includes('#') || paragraphText.includes('`')) // don't process latex if a codespan or header tag
&& !/^[\w\s.()[\]{}]+$/.test(paragraphText) // don't process latex if string only contains alphanumeric chars, whitespace, periods, and brackets
) {
const rawText = paragraphText;
// Regular expressions to match LaTeX delimiters
const displayMathRegex = /\$\$(.*?)\$\$/g; // Display math: $$...$$
const inlineMathRegex = /\$((?!\$).*?)\$/g; // Inline math: $...$ (but not $$)
// Check if the paragraph contains any LaTeX expressions
if (displayMathRegex.test(rawText) || inlineMathRegex.test(rawText)) {
// Reset the regex state (since we used .test earlier)
displayMathRegex.lastIndex = 0;
inlineMathRegex.lastIndex = 0;
// Parse the text into segments of regular text and LaTeX
let lastIndex = 0;
let segmentId = 0;
// First replace display math ($$...$$)
let match;
while ((match = displayMathRegex.exec(rawText)) !== null) {
const [fullMatch, formula] = match;
const matchIndex = match.index;
// Add text before the LaTeX expression
if (matchIndex > lastIndex) {
const textBefore = rawText.substring(lastIndex, matchIndex);
segments.push(
<span key={`text-${segmentId++}`}>
{textBefore}
</span>
);
}
// Add the LaTeX expression
segments.push(
<LatexRender key={`latex-${segmentId++}`} latex={fullMatch} />
);
lastIndex = matchIndex + fullMatch.length;
}
// Add any remaining text (which might contain inline math)
if (lastIndex < rawText.length) {
const remainingText = rawText.substring(lastIndex);
// Process inline math in the remaining text
lastIndex = 0;
inlineMathRegex.lastIndex = 0;
const inlineSegments: React.ReactNode[] = [];
while ((match = inlineMathRegex.exec(remainingText)) !== null) {
const [fullMatch] = match;
const matchIndex = match.index;
// Add text before the inline LaTeX
if (matchIndex > lastIndex) {
const textBefore = remainingText.substring(lastIndex, matchIndex);
inlineSegments.push(
<span key={`inline-text-${segmentId++}`}>
{textBefore}
</span>
);
}
// Add the inline LaTeX
inlineSegments.push(
<LatexRender key={`inline-latex-${segmentId++}`} latex={fullMatch} />
);
lastIndex = matchIndex + fullMatch.length;
}
// Add any remaining text after all inline math
if (lastIndex < remainingText.length) {
inlineSegments.push(
<span key={`inline-final-${segmentId++}`}>
{remainingText.substring(lastIndex)}
</span>
);
}
segments.push(...inlineSegments);
}
}
}
return segments
}
export type RenderTokenOptions = { isApplyEnabled?: boolean, isLinkDetectionEnabled?: boolean }
const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ...options }: { token: Token | string, inPTag?: boolean, codeURI?: URI, chatMessageLocation?: ChatMessageLocation, tokenIdx: string, } & RenderTokenOptions): React.ReactNode => {
const accessor = useAccessor()
@ -189,24 +348,25 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
}
if (t.type === 'table') {
return (
<div>
<table>
<thead>
<tr>
{t.header.map((cell: any, index: number) => (
<th key={index}>
{cell.raw}
{t.header.map((h, hIdx: number) => (
<th key={hIdx}>
{h.text}
</th>
))}
</tr>
</thead>
<tbody>
{t.rows.map((row: any[], rowIndex: number) => (
<tr key={rowIndex}>
{row.map((cell: any, cellIndex: number) => (
<td key={cellIndex} >
{cell.raw}
{t.rows.map((row, rowIdx: number) => (
<tr key={rowIdx}>
{row.map((r, rIdx: number) => (
<td key={rIdx} >
{r.text}
</td>
))}
</tr>
@ -288,6 +448,17 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
}
if (t.type === 'paragraph') {
// check for latex
const latexSegments = paragraphToLatexSegments(t.raw)
if (latexSegments.length !== 0) {
if (inPTag) {
return <span className='block'>{latexSegments}</span>;
}
return <p>{latexSegments}</p>;
}
// if no latex, default behavior
const contents = <>
{t.tokens.map((token, index) => (
<RenderToken key={index}
@ -384,4 +555,3 @@ export const ChatMarkdownRender = ({ string, inPTag = false, chatMessageLocation
</>
)
}

View file

@ -14,7 +14,7 @@ import { IDisposable } from '../../../../../../../base/common/lifecycle.js';
import { ErrorDisplay } from './ErrorDisplay.js';
import { BlockCode, TextAreaFns, VoidCustomDropdownBox, VoidInputBox2, VoidSlider, VoidSwitch } from '../util/inputs.js';
import { ModelDropdown, } from '../void-settings-tsx/ModelDropdown.js';
import { SidebarThreadSelector } from './SidebarThreadSelector.js';
import { OldSidebarThreadSelector, PastThreadsList } from './SidebarThreadSelector.js';
import { VOID_CTRL_L_ACTION_ID } from '../../../actionIDs.js';
import { VOID_OPEN_SETTINGS_ACTION_ID } from '../../../voidSettingsPane.js';
import { ChatMode, displayInfoOfProviderName, FeatureName, isFeatureNameDisabled } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js';
@ -29,6 +29,7 @@ import { acceptAllBg, acceptBorder, buttonFontSize, buttonTextColor, rejectAllBg
import { ToolName, toolNames } from '../../../../common/prompt/prompts.js';
import { RawToolCallObj } from '../../../../common/sendLLMMessageTypes.js';
import { MAX_FILE_CHARS_PAGE } from '../../../toolsService.js';
import jsonStringify from 'fast-json-stable-stringify'
import ErrorBoundary from './ErrorBoundary.js';
@ -143,9 +144,6 @@ export const IconLoading = ({ className = '' }: { className?: string }) => {
}
const getChatBubbleId = (threadId: string, messageIdx: number) => `${threadId}-${messageIdx}`;
// SLIDER ONLY:
const ReasoningOptionSlider = ({ featureName }: { featureName: FeatureName }) => {
@ -554,8 +552,7 @@ export const SelectedFiles = (
{allSelections.map((selection, i) => {
const isThisSelectionProspective = i > selections.length - 1
const thisKey = `${isThisSelectionProspective}-${i}-${selections.length}`
const thisKey = jsonStringify(selection)
return <div // container for summarybox and code
key={thisKey}
@ -1979,7 +1976,13 @@ type ChatBubbleProps = {
_scrollToBottom: (() => void) | null,
}
const ChatBubble = ({ threadId, chatMessage, currCheckpointIdx, isCommitted, messageIdx, chatIsRunning, _scrollToBottom }: ChatBubbleProps) => {
const ChatBubble = (props: ChatBubbleProps) => {
return <ErrorBoundary>
<_ChatBubble {...props} />
</ErrorBoundary>
}
const _ChatBubble = ({ threadId, chatMessage, currCheckpointIdx, isCommitted, messageIdx, chatIsRunning, _scrollToBottom }: ChatBubbleProps) => {
const role = chatMessage.role
const isCheckpointGhost = messageIdx > (currCheckpointIdx ?? Infinity) && !chatIsRunning // whether to show as gray (if chat is running, for good measure just dont show any ghosts)
@ -2001,33 +2004,6 @@ const ChatBubble = ({ threadId, chatMessage, currCheckpointIdx, isCommitted, mes
isCommitted={isCommitted}
/>
}
// else if (role === 'tool_request') {
// const ToolRequestWrapper = toolNameToComponent[chatMessage.name]?.requestWrapper as RequestWrapper<ToolName>
// const toolRequestState = (
// chatIsRunning === 'awaiting_user' ? 'awaiting_user'
// : chatIsRunning === 'tool' ? 'running'
// : chatIsRunning === 'message' ? null
// : null
// )
// if (ToolRequestWrapper && canAcceptReject) { // if it's the last message
// return <>
// {toolRequestState !== null &&
// <div className={`${isCheckpointGhost ? 'opacity-50' : ''}`}>
// <ToolRequestWrapper
// toolRequestState={toolRequestState}
// toolRequest={chatMessage}
// messageIdx={messageIdx}
// threadId={threadId}
// />
// </div>}
// {chatIsRunning === 'awaiting_user' &&
// <div className={`${isCheckpointGhost ? 'opacity-50 pointer-events-none' : ''}`}>
// <ToolRequestAcceptRejectButtons />
// </div>}
// </>
// }
// return null
// }
else if (role === 'tool') {
if (chatMessage.type === 'invalid_params') {
@ -2537,8 +2513,8 @@ export const SidebarChat = () => {
// const lastMessageIdx = previousMessages.findLastIndex(v => v.role !== 'checkpoint')
// tool request shows up as Editing... if in progress
return previousMessages.map((message, i) => {
return <ErrorBoundary><ChatBubble
key={getChatBubbleId(threadId, i)}
return <ChatBubble
key={i}
currCheckpointIdx={currCheckpointIdx}
chatMessage={message}
messageIdx={i}
@ -2546,14 +2522,14 @@ export const SidebarChat = () => {
chatIsRunning={isRunning}
threadId={threadId}
_scrollToBottom={() => scrollToBottom(scrollContainerRef)}
/></ErrorBoundary>
/>
})
}, [previousMessages, threadId, currCheckpointIdx, isRunning])
const streamingChatIdx = previousMessagesHTML.length
const currStreamingMessageHTML = reasoningSoFar || displayContentSoFar || isRunning ?
<ErrorBoundary><ChatBubble
key={getChatBubbleId(threadId, streamingChatIdx)}
<ChatBubble
key={'curr-streaming-msg'}
currCheckpointIdx={currCheckpointIdx}
chatMessage={{
role: 'assistant',
@ -2567,13 +2543,13 @@ export const SidebarChat = () => {
threadId={threadId}
_scrollToBottom={null}
/></ErrorBoundary> : null
/> : null
// the tool currently being generated
const generatingTool = toolIsGenerating ?
toolCallSoFar.name === 'edit_file' ? <EditToolSoFar
key={getChatBubbleId(threadId, streamingChatIdx + 1)}
key={'curr-streaming-tool'}
toolCallSoFar={toolCallSoFar}
/>
: null
@ -2631,61 +2607,106 @@ export const SidebarChat = () => {
}
}, [onSubmit, onAbort, isRunning])
const inputForm = <div key={'input' + chatThreadsState.currentThreadId}>
<div className='px-4'>
{previousMessages.length > 0 &&
<CommandBarInChat />
}
</div>
<div
className='px-2 pb-2'
>
<VoidChatArea
featureName='Chat'
onSubmit={onSubmit}
onAbort={onAbort}
isStreaming={!!isRunning}
isDisabled={isDisabled}
showSelections={true}
showProspectiveSelections={previousMessagesHTML.length === 0}
selections={selections}
setSelections={setSelections}
onClickAnywhere={() => { textAreaRef.current?.focus() }}
>
<VoidInputBox2
className={`min-h-[81px] px-0.5 py-0.5`}
placeholder={`${keybindingString ? `${keybindingString} to add a file. ` : ''}Enter instructions...`}
onChangeText={onChangeText}
onKeyDown={onKeyDown}
onFocus={() => { chatThreadsService.setCurrentlyFocusedMessageIdx(undefined) }}
ref={textAreaRef}
fnsRef={textAreaFnsRef}
multiline={true}
/>
</VoidChatArea>
const inputChatArea = <VoidChatArea
featureName='Chat'
onSubmit={onSubmit}
onAbort={onAbort}
isStreaming={!!isRunning}
isDisabled={isDisabled}
showSelections={true}
showProspectiveSelections={previousMessagesHTML.length === 0}
selections={selections}
setSelections={setSelections}
onClickAnywhere={() => { textAreaRef.current?.focus() }}
>
<VoidInputBox2
className={`min-h-[81px] px-0.5 py-0.5`}
placeholder={`${keybindingString ? `${keybindingString} to add a file. ` : ''}Enter instructions...`}
onChangeText={onChangeText}
onKeyDown={onKeyDown}
onFocus={() => { chatThreadsService.setCurrentlyFocusedMessageIdx(undefined) }}
ref={textAreaRef}
fnsRef={textAreaFnsRef}
multiline={true}
/>
</VoidChatArea>
const isLandingPage = previousMessages.length === 0
const threadPageInput = <div key={'input' + chatThreadsState.currentThreadId}>
<div className='px-4'>
<CommandBarInChat />
</div>
<div className='px-2 pb-2'>
{inputChatArea}
</div>
</div>
return (
<div ref={sidebarRef} className='w-full h-full flex flex-col overflow-hidden'>
{/* History selector */}
<div className={`w-full ${isHistoryOpen ? '' : 'hidden'} ring-2 ring-widget-shadow ring-inset z-10`}>
<ErrorBoundary>
<SidebarThreadSelector />
</ErrorBoundary>
</div>
<div className='flex-1 flex flex-col overflow-hidden'>
<div className={`flex-1 overflow-hidden ${previousMessages.length === 0 ? 'h-0 max-h-0 pb-2' : ''}`}>
<ErrorBoundary>
{messagesHTML}
</ErrorBoundary>
</div>
<ErrorBoundary>
{inputForm}
</ErrorBoundary>
</div>
const landingPageInput = <div>
<div className='pt-8'>
{inputChatArea}
</div>
</div>
const landingPageContent = <div
ref={sidebarRef}
className='w-full h-full max-h-full flex flex-col overflow-auto px-4'
>
<ErrorBoundary>
{landingPageInput}
</ErrorBoundary>
{Object.values(chatThreadsState.allThreads).length > 0 && // show if there are threads
<ErrorBoundary>
<div className='pt-8 mb-2 text-void-fg-1 text-root'>Previous Threads</div>
<PastThreadsList />
</ErrorBoundary>
}
</div>
// const threadPageContent = <div>
// {/* Thread content */}
// <div className='flex flex-col overflow-hidden'>
// <div className={`overflow-hidden ${previousMessages.length === 0 ? 'h-0 max-h-0 pb-2' : ''}`}>
// <ErrorBoundary>
// {messagesHTML}
// </ErrorBoundary>
// </div>
// <ErrorBoundary>
// {inputForm}
// </ErrorBoundary>
// </div>
// </div>
const threadPageContent = <div
ref={sidebarRef}
className='w-full h-full flex flex-col overflow-hidden'
>
<ErrorBoundary>
{messagesHTML}
</ErrorBoundary>
<ErrorBoundary>
{threadPageInput}
</ErrorBoundary>
</div>
return (
<Fragment key={threadId} // force rerender when change thread
>
{isLandingPage ?
landingPageContent
: threadPageContent}
</Fragment>
)
}

View file

@ -3,35 +3,20 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import React from "react";
import { useState } from 'react';
import { IconShell1 } from '../markdown/ApplyBlockHoverButtons.js';
import { useAccessor, useChatThreadsState } from '../util/services.js';
import { ISidebarStateService } from '../../../sidebarStateService.js';
import { IconX } from './SidebarChat.js';
import { Check, Trash2, X } from 'lucide-react';
import { ThreadType } from '../../../chatThreadService.js';
const truncate = (s: string) => {
let len = s.length
const TRUNC_AFTER = 16
if (len >= TRUNC_AFTER)
s = s.substring(0, TRUNC_AFTER) + '...'
return s
}
export const OldSidebarThreadSelector = () => {
export const SidebarThreadSelector = () => {
const threadsState = useChatThreadsState()
const accessor = useAccessor()
const chatThreadsService = accessor.get('IChatThreadService')
const sidebarStateService = accessor.get('ISidebarStateService')
const { allThreads } = threadsState
// sorted by most recent to least recent
const sortedThreadIds = Object.keys(allThreads ?? {})
.sort((threadId1, threadId2) => (allThreads[threadId1]?.lastModified ?? 0) > (allThreads[threadId2]?.lastModified ?? 0) ? -1 : 1)
.filter(threadId => (allThreads![threadId]?.messages.length ?? 0) !== 0)
return (
<div className="flex p-2 flex-col gap-y-1 max-h-[200px] overflow-y-auto">
@ -52,72 +37,297 @@ export const SidebarThreadSelector = () => {
</div>
{/* a list of all the past threads */}
<div className="px-1">
<ul className="flex flex-col gap-y-0.5 overflow-y-auto list-disc">
{sortedThreadIds.length === 0
? <div key="nothreads" className="text-center text-void-fg-3 brightness-90 text-sm">{`There are no chat threads yet.`}</div>
: sortedThreadIds.map((threadId) => {
if (!allThreads) {
return <li key="error" className="text-void-warning">{`Error accessing chat history.`}</li>;
}
const pastThread = allThreads[threadId];
if (!pastThread) {
return <li key="error" className="text-void-warning">{`Error accessing chat history.`}</li>;
}
let firstMsg = null;
// let secondMsg = null;
const firstUserMsgIdx = pastThread.messages.findIndex((msg) => msg.role === 'user');
if (firstUserMsgIdx !== -1) {
// firstMsg = truncate(pastThread.messages[firstMsgIdx].displayContent ?? '');
const firsUsertMsgObj = pastThread.messages[firstUserMsgIdx]
firstMsg = firsUsertMsgObj.role === 'user' && firsUsertMsgObj.displayContent || '';
} else {
firstMsg = '""';
}
// const secondMsgIdx = pastThread.messages.findIndex(
// (msg, i) => msg.role !== 'system' && !!msg.displayContent && i > firstMsgIdx
// );
// if (secondMsgIdx !== -1) {
// secondMsg = truncate(pastThread.messages[secondMsgIdx].displayContent ?? '');
// }
const numMessages = pastThread.messages.filter((msg) => msg.role === 'assistant' || msg.role === 'user').length;
return (
<li key={pastThread.id}>
<button
type='button'
className={`
hover:bg-void-bg-1
${threadsState.currentThreadId === pastThread.id ? 'bg-void-bg-1' : ''}
rounded-sm px-2 py-1
w-full
text-left
flex items-center
`}
onClick={() => chatThreadsService.switchToThread(pastThread.id)}
onDoubleClick={() => sidebarStateService.setState({ isHistoryOpen: false })}
title={new Date(pastThread.lastModified).toLocaleString()}
>
<div className='truncate'>{`${firstMsg}`}</div>
<div>{`\u00A0(${numMessages})`}</div>
</button>
</li>
);
})
}
</ul>
</div>
{/* <OldPastThreadsList /> */}
</div>
)
}
const truncate = (s: string) => {
let len = s.length
const TRUNC_AFTER = 16
if (len >= TRUNC_AFTER)
s = s.substring(0, TRUNC_AFTER) + '...'
return s
}
const OldPastThreadsList = () => {
const accessor = useAccessor()
const chatThreadsService = accessor.get('IChatThreadService')
const sidebarStateService = accessor.get('ISidebarStateService')
const threadsState = useChatThreadsState()
const { allThreads } = threadsState
// sorted by most recent to least recent
const sortedThreadIds = Object.keys(allThreads ?? {})
.sort((threadId1, threadId2) => (allThreads[threadId1]?.lastModified ?? 0) > (allThreads[threadId2]?.lastModified ?? 0) ? -1 : 1)
.filter(threadId => (allThreads![threadId]?.messages.length ?? 0) !== 0)
return <div className="px-1">
<ul className="flex flex-col gap-y-0.5 overflow-y-auto list-disc">
{sortedThreadIds.length === 0
? <div key="nothreads" className="text-center text-void-fg-3 brightness-90 text-root">{`There are no chat threads yet.`}</div>
: sortedThreadIds.map((threadId) => {
if (!allThreads) {
return <li key="error" className="text-void-warning">{`Error accessing chat history.`}</li>;
}
const pastThread = allThreads[threadId];
if (!pastThread) {
return <li key="error" className="text-void-warning">{`Error accessing chat history.`}</li>;
}
let firstMsg = null;
// let secondMsg = null;
const firstUserMsgIdx = pastThread.messages.findIndex((msg) => msg.role === 'user');
if (firstUserMsgIdx !== -1) {
// firstMsg = truncate(pastThread.messages[firstMsgIdx].displayContent ?? '');
const firsUsertMsgObj = pastThread.messages[firstUserMsgIdx]
firstMsg = firsUsertMsgObj.role === 'user' && firsUsertMsgObj.displayContent || '';
} else {
firstMsg = '""';
}
// const secondMsgIdx = pastThread.messages.findIndex(
// (msg, i) => msg.role !== 'system' && !!msg.displayContent && i > firstMsgIdx
// );
// if (secondMsgIdx !== -1) {
// secondMsg = truncate(pastThread.messages[secondMsgIdx].displayContent ?? '');
// }
const numMessages = pastThread.messages.filter((msg) => msg.role === 'assistant' || msg.role === 'user').length;
return (
<li key={pastThread.id}>
<button
type='button'
className={`
hover:bg-void-bg-1
${threadsState.currentThreadId === pastThread.id ? 'bg-void-bg-1' : ''}
rounded-sm px-2 py-1
w-full
text-left
flex items-center
`}
onClick={() => {
chatThreadsService.switchToThread(pastThread.id);
sidebarStateService.setState({ isHistoryOpen: false })
}}
title={new Date(pastThread.lastModified).toLocaleString()}
>
<div className='truncate'>{`${firstMsg}`}</div>
<div>{`\u00A0(${numMessages})`}</div>
</button>
</li>
);
})
}
</ul>
</div>
}
const numInitialThreads = 3
export const PastThreadsList = ({ className = '' }: { className?: string }) => {
const [showAll, setShowAll] = useState(false);
const [hoveredIdx, setHoveredIdx] = useState<number | null>(null)
const threadsState = useChatThreadsState()
const { allThreads } = threadsState
if (!allThreads) {
return <div key="error" className="p-1">{`Error accessing chat history.`}</div>;
}
// sorted by most recent to least recent
const sortedThreadIds = Object.keys(allThreads ?? {})
.sort((threadId1, threadId2) => (allThreads[threadId1]?.lastModified ?? 0) > (allThreads[threadId2]?.lastModified ?? 0) ? -1 : 1)
.filter(threadId => (allThreads![threadId]?.messages.length ?? 0) !== 0)
// Get only first 5 threads if not showing all
const hasMoreThreads = sortedThreadIds.length > numInitialThreads;
const displayThreads = showAll ? sortedThreadIds : sortedThreadIds.slice(0, numInitialThreads);
return (
<div className={`flex flex-col mb-2 gap-2 w-full text-nowrap text-void-fg-3 select-none relative ${className}`}>
{displayThreads.length === 0
? <></> // No chats yet... Suggestion: Tell me about my codebase Suggestion: Create a new .voidrules file in the root of my repo
: displayThreads.map((threadId, i) => {
const pastThread = allThreads[threadId];
if (!pastThread) {
return <div key={i} className="p-1">{`Error accessing chat history.`}</div>;
}
return (
<PastThreadElement
key={pastThread.id}
pastThread={pastThread}
idx={i}
hoveredIdx={hoveredIdx}
setHoveredIdx={setHoveredIdx}
/>
);
})
}
{hasMoreThreads && !showAll && (
<div
className="text-void-fg-3 opacity-60 hover:opacity-100 hover:brightness-115 cursor-pointer p-1 text-xs"
onClick={() => setShowAll(true)}
>
Show {sortedThreadIds.length - numInitialThreads} more...
</div>
)}
{hasMoreThreads && showAll && (
<div
className="text-void-fg-3 opacity-60 hover:opacity-100 hover:brightness-115 cursor-pointer p-1 text-xs"
onClick={() => setShowAll(false)}
>
Show less
</div>
)}
</div>
);
};
// Format date to display as today, yesterday, or date
const formatDate = (date: Date) => {
const now = new Date();
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
const yesterday = new Date(today);
yesterday.setDate(yesterday.getDate() - 1);
if (date >= today) {
return 'Today';
} else if (date >= yesterday) {
return 'Yesterday';
} else {
return `${date.toLocaleString('default', { month: 'short' })} ${date.getDate()}`;
}
};
// Format time to 12-hour format
const formatTime = (date: Date) => {
return date.toLocaleString('en-US', {
hour: 'numeric',
minute: '2-digit',
hour12: true
});
};
const TrashButton = ({ threadId }: { threadId: string }) => {
const accessor = useAccessor()
const chatThreadsService = accessor.get('IChatThreadService')
const [isTrashPressed, setIsTrashPressed] = useState(false)
return (isTrashPressed ?
<div className='flex flex-nowrap text-nowrap gap-1'>
<IconShell1
Icon={X}
className='size-[11px]'
onClick={() => { setIsTrashPressed(false); }}
data-tooltip-id='void-tooltip'
data-tooltip-place='top'
data-tooltip-content='Cancel'
/>
<IconShell1
Icon={Check}
className='size-[11px]'
onClick={() => { chatThreadsService.deleteThread(threadId); setIsTrashPressed(false); }}
data-tooltip-id='void-tooltip'
data-tooltip-place='top'
data-tooltip-content='Confirm'
/>
</div>
: <IconShell1
Icon={Trash2}
className='size-[11px]'
onClick={() => { setIsTrashPressed(true); }}
data-tooltip-id='void-tooltip'
data-tooltip-place='top'
data-tooltip-content='Delete thread?'
/>
)
}
const PastThreadElement = ({ pastThread, idx, hoveredIdx, setHoveredIdx }: { pastThread: ThreadType, idx: number, hoveredIdx: number | null, setHoveredIdx: (idx: number | null) => void }) => {
const accessor = useAccessor()
const chatThreadsService = accessor.get('IChatThreadService')
const sidebarStateService = accessor.get('ISidebarStateService')
let firstMsg = null;
const firstUserMsgIdx = pastThread.messages.findIndex((msg) => msg.role === 'user');
if (firstUserMsgIdx !== -1) {
const firsUsertMsgObj = pastThread.messages[firstUserMsgIdx];
firstMsg = firsUsertMsgObj.role === 'user' && firsUsertMsgObj.displayContent || '';
} else {
firstMsg = '""';
}
const numMessages = pastThread.messages.filter((msg) => msg.role === 'assistant' || msg.role === 'user').length;
const detailsHTML = <span
className='gap-1 inline-flex items-center'
// data-tooltip-id='void-tooltip'
// data-tooltip-content={`Last modified ${formatTime(new Date(pastThread.lastModified))}`}
// data-tooltip-place='top'
>
{/* <span>{numMessages}</span> */}
{formatDate(new Date(pastThread.lastModified))}
</span>
return <div
key={pastThread.id}
className={`
py-1 px-2 rounded text-sm bg-zinc-700/5 hover:bg-zinc-700/10 dark:bg-zinc-300/5 dark:hover:bg-zinc-300/10 cursor-pointer opacity-80 hover:opacity-100
`}
onClick={() => {
chatThreadsService.switchToThread(pastThread.id);
sidebarStateService.setState({ isHistoryOpen: false });
}}
onMouseEnter={() => setHoveredIdx(idx)}
onMouseLeave={() => setHoveredIdx(null)}
>
<div className="flex items-center justify-between gap-1">
<span className="flex items-center gap-2 min-w-0 overflow-hidden">
<span className="truncate overflow-hidden text-ellipsis">{firstMsg}</span>
</span>
<div className="flex items-center gap-2 opacity-60">
{idx === hoveredIdx ?
<TrashButton threadId={pastThread.id} />
: detailsHTML
}
</div>
</div>
</div>
}

View file

@ -12,7 +12,7 @@
--void-bg-1-alt: var(--vscode-badge-background);
--void-bg-2: var(--vscode-sideBar-background);
--void-bg-2-alt: color-mix(in srgb, var(--vscode-editor-background) 30%, var(--vscode-sideBar-background) 70%);
--void-bg-2-hover: color-mix(in srgb, var(--vscode-editor-foreground) 5%, var(--vscode-sideBar-background) 95%);
--void-bg-2-hover: color-mix(in srgb, var(--vscode-editor-foreground) 2%, var(--vscode-sideBar-background) 98%);
--void-bg-3: var(--vscode-editor-background);
--void-fg-0: color-mix(in srgb, var(--vscode-tab-activeForeground) 90%, black 10%);

View file

@ -664,7 +664,7 @@ export const VoidCustomDropdownBox = <T extends NonNullable<any>>({
{isOpen && (
<div
ref={refs.setFloating}
className="z-10 bg-void-bg-1 border-void-border-3 border rounded shadow-lg"
className="z-[100] bg-void-bg-1 border-void-border-3 border rounded shadow-lg"
style={{
position: strategy,
top: y ?? 0,
@ -689,7 +689,7 @@ export const VoidCustomDropdownBox = <T extends NonNullable<any>>({
key={optionName}
className={`flex items-center px-2 py-1 pr-4 cursor-pointer whitespace-nowrap
transition-all duration-100
${thisOptionIsSelected ? 'bg-void-bg-2-hover' : 'bg-void-bg-2 hover:bg-void-bg-2-hover'}
${thisOptionIsSelected ? 'bg-void-bg-2-hover' : 'bg-void-bg-2-alt hover:bg-void-bg-2-hover'}
`}
onClick={() => {
onChangeOption(option);
@ -709,7 +709,7 @@ export const VoidCustomDropdownBox = <T extends NonNullable<any>>({
</svg>
)}
</div>
<span className="flex justify-between w-full">
<span className="flex justify-between items-center w-full gap-x-1">
<span>{optionName}</span>
<span className='text-void-fg-4 opacity-60'>{optionDetail}</span>
</span>

View file

@ -307,7 +307,7 @@ const TableOfModelsForProvider = ({ providerName }: { providerName: ProviderName
voidSettingsState.settingsOfProvider[providerName].models.forEach(m => {
infoOfModelName[m.modelName] = {
showAsDefault: m.isDefault,
showAsDefault: m.type === 'default',
isDownloaded: true
}
})
@ -367,7 +367,7 @@ const TableOfModelsForProvider = ({ providerName }: { providerName: ProviderName
return (
<tr key={modelName} className="border-b border-void-border-1 hover:bg-void-bg-3/50">
<tr key={`${modelName}${providerName}`} className="border-b border-void-border-1 hover:bg-void-bg-3/50">
<td className="py-2 px-3 relative">
{!showAsDefault && removeModelButton}
{modelName}
@ -497,7 +497,7 @@ const VoidOnboardingContent = () => {
const providerNamesOfWantToUseOption: { [wantToUseOption in WantToUseOption]: ProviderName[] } = {
smart: ['anthropic', 'openAI', 'gemini', 'openRouter'],
private: ['ollama', 'vLLM', 'openAICompatible'],
private: ['ollama', 'vLLM', 'openAICompatible', 'lmStudio'],
cheap: ['gemini', 'deepseek', 'openRouter', 'ollama', 'vLLM'],
all: providerNames,
}

View file

@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------*/
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { ProviderName, SettingName, displayInfoOfSettingName, providerNames, VoidStatefulModelInfo, customSettingNamesOfProvider, RefreshableProviderName, refreshableProviderNames, displayInfoOfProviderName, nonlocalProviderNames, localProviderNames, GlobalSettingName, featureNames, displayInfoOfFeatureName, isProviderNameDisabled, FeatureName, hasDownloadButtonsOnModelsProviderNames } from '../../../../common/voidSettingsTypes.js'
import { ProviderName, SettingName, displayInfoOfSettingName, providerNames, VoidStatefulModelInfo, customSettingNamesOfProvider, RefreshableProviderName, refreshableProviderNames, displayInfoOfProviderName, nonlocalProviderNames, localProviderNames, GlobalSettingName, featureNames, displayInfoOfFeatureName, isProviderNameDisabled, FeatureName, hasDownloadButtonsOnModelsProviderNames, subTextMdOfProviderName } from '../../../../common/voidSettingsTypes.js'
import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js'
import { VoidButtonBgDarken, VoidCustomDropdownBox, VoidInputBox2, VoidSimpleInputBox, VoidSwitch } from '../util/inputs.js'
import { useAccessor, useIsDark, useRefreshModelListener, useRefreshModelState, useSettingsState } from '../util/services.js'
@ -286,7 +286,7 @@ export const ModelDump = () => {
return <div className=''>
{modelDump.map((m, i) => {
const { isHidden, isDefault, isAutodetected, modelName, providerName, providerEnabled } = m
const { isHidden, type, modelName, providerName, providerEnabled } = m
const isNewProviderName = (i > 0 ? modelDump[i - 1] : undefined)?.providerName !== providerName
@ -318,7 +318,7 @@ export const ModelDump = () => {
// : (isHidden ? `'${modelName}' won't appear in dropdowns` : ``)
// }
>
<span className='opacity-50 truncate'>{isAutodetected ? '(detected locally)' : isDefault ? '' : '(custom model)'}</span>
<span className='opacity-50 truncate'>{type === 'autodetected' ? '(detected locally)' : type === 'default' ? '' : '(custom model)'}</span>
<VoidSwitch
value={value}
@ -332,7 +332,7 @@ export const ModelDump = () => {
/>
<div className={`w-5 flex items-center justify-center`}>
{isDefault ? null : <button onClick={() => { settingsStateService.deleteModel(providerName, modelName) }}><X className='size-4' /></button>}
{type === 'default' || type === 'autodetected' ? null : <button onClick={() => { settingsStateService.deleteModel(providerName, modelName) }}><X className='size-4' /></button>}
</div>
</div>
</div>
@ -344,9 +344,9 @@ export const ModelDump = () => {
// providers
const ProviderSetting = ({ providerName, settingName }: { providerName: ProviderName, settingName: SettingName }) => {
const ProviderSetting = ({ providerName, settingName, subTextMd }: { providerName: ProviderName, settingName: SettingName, subTextMd: React.ReactNode }) => {
const { title: settingTitle, placeholder, isPasswordField, subTextMd } = displayInfoOfSettingName(providerName, settingName)
const { title: settingTitle, placeholder, isPasswordField } = displayInfoOfSettingName(providerName, settingName)
const accessor = useAccessor()
const voidSettingsService = accessor.get('IVoidSettingsService')
@ -370,10 +370,9 @@ const ProviderSetting = ({ providerName, settingName }: { providerName: Provider
passwordBlur={isPasswordField}
compact={true}
/>
{subTextMd === undefined ? null : <div className='py-1 px-3 opacity-50 text-sm'>
<ChatMarkdownRender string={subTextMd} chatMessageLocation={undefined} />
{!subTextMd ? null : <div className='py-1 px-3 opacity-50 text-sm'>
{subTextMd}
</div>}
</div>
</ErrorBoundary>
}
@ -456,7 +455,14 @@ export const SettingsForProvider = ({ providerName, showProviderTitle, showProvi
<div className='px-0'>
{/* settings besides models (e.g. api key) */}
{settingNames.map((settingName, i) => {
return <ProviderSetting key={settingName} providerName={providerName} settingName={settingName} />
return <ProviderSetting
key={settingName}
providerName={providerName}
settingName={settingName}
subTextMd={i !== settingNames.length - 1 ? null
: <ChatMarkdownRender string={subTextMdOfProviderName(providerName)} chatMessageLocation={undefined} />}
/>
})}
{showProviderSuggestions && needsModel ?
@ -1025,11 +1031,11 @@ export const Settings = () => {
<div className='mt-12 max-w-[600px]'>
<h2 className={`text-3xl mb-2`}>AI Instructions</h2>
<h4 className={`text-void-fg-3 mb-4`}>
<ChatMarkdownRender inPTag={true} string={`
<ChatMarkdownRender inPTag={true} string={`
System instructions to include with all AI requests.
Alternatively, place a \`.voidinstructions\` file in the root of your workspace.
Alternatively, place a \`.voidrules\` file in the root of your workspace.
`} chatMessageLocation={undefined} />
</h4>
</h4>
<ErrorBoundary>
<AIInstructionsBox />
</ErrorBoundary>

View file

@ -202,7 +202,19 @@ registerAction2(class extends Action2 {
})
const openNewThreadAndFireFocus = (accessor: ServicesAccessor) => {
const stateService = accessor.get(ISidebarStateService)
stateService.setState({ isHistoryOpen: false, currentTab: 'chat' })
const chatThreadService = accessor.get(IChatThreadService)
chatThreadService.openNewThread()
// focus
stateService.fireFocusChat()
const window = getActiveWindow()
window.requestAnimationFrame(() => stateService.fireFocusChat())
}
// New chat menu button
@ -213,6 +225,25 @@ registerAction2(class extends Action2 {
title: 'New Chat',
icon: { id: 'add' },
menu: [{ id: MenuId.ViewTitle, group: 'navigation', when: ContextKeyExpr.equals('view', VOID_VIEW_ID), }],
});
}
async run(accessor: ServicesAccessor): Promise<void> {
const metricsService = accessor.get(IMetricsService)
metricsService.capture('Chat Navigation', { type: 'New Chat' })
openNewThreadAndFireFocus(accessor)
}
})
// New chat keybind
registerAction2(class extends Action2 {
constructor() {
super({
id: 'void.newChatKeybindAction',
title: 'New Chat Keybind',
keybinding: {
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyL,
weight: KeybindingWeight.VoidExtension,
@ -220,19 +251,16 @@ registerAction2(class extends Action2 {
});
}
async run(accessor: ServicesAccessor): Promise<void> {
const stateService = accessor.get(ISidebarStateService)
const metricsService = accessor.get(IMetricsService)
const commandService = accessor.get(ICommandService)
metricsService.capture('Chat Navigation', { type: 'New Chat Keybind' })
metricsService.capture('Chat Navigation', { type: 'New Chat' })
openNewThreadAndFireFocus(accessor)
stateService.setState({ isHistoryOpen: false, currentTab: 'chat' })
const chatThreadService = accessor.get(IChatThreadService)
chatThreadService.openNewThread()
// add user's selection to chat
await commandService.executeCommand(VOID_CTRL_L_ACTION_ID)
// focus
stateService.fireFocusChat()
const window = getActiveWindow()
window.requestAnimationFrame(() => stateService.fireFocusChat())
}
})
@ -247,13 +275,27 @@ registerAction2(class extends Action2 {
});
}
async run(accessor: ServicesAccessor): Promise<void> {
// do not do anything if there are no messages (without this it clears all of the user's selections if the button is pressed)
// TODO the history button should be disabled in this case so we can remove this logic
const thread = accessor.get(IChatThreadService).getCurrentThread()
if (thread.messages.length === 0) {
return;
}
const stateService = accessor.get(ISidebarStateService)
const metricsService = accessor.get(IMetricsService)
metricsService.capture('Chat Navigation', { type: 'History' })
openNewThreadAndFireFocus(accessor)
// doesnt do anything right now
stateService.setState({ isHistoryOpen: !stateService.state.isHistoryOpen, currentTab: 'chat' })
stateService.fireBlurChat()
}
})

View file

@ -13,7 +13,7 @@ import { VOID_OPEN_SIDEBAR_ACTION_ID } from './sidebarPane.js';
// service that manages sidebar's state
export type VoidSidebarState = {
isHistoryOpen: boolean;
isHistoryOpen: boolean; // this isn't doing anything right now
currentTab: 'chat';
}

View file

@ -263,7 +263,7 @@ export class ToolsService implements IToolsService {
read_file: async ({ uri, startLine, endLine, pageNumber }) => {
await voidModelService.initializeModel(uri)
const { model } = await voidModelService.getModelSafe(uri)
if (model === null) { throw new Error(`Contents were empty. There may have been an error, or the file may not exist.`) }
if (model === null) { throw new Error(`No contents; File does not exist.`) }
let contents: string
if (startLine === null && endLine === null) {

View file

@ -153,6 +153,7 @@ export const extractCodeFromFIM = ({ text, recentlyAddedTextLen, midTag, }: { te
const foundMid = pm.removePrefix(`<${midTag}>`)
if (foundMid) {
pm.removeSuffix(`\n`) // sometimes outputs \n
pm.removeSuffix(`</${midTag}>`)
}
const s = pm.value()

View file

@ -43,7 +43,22 @@ export const defaultProviderSettings = {
},
mistral: {
apiKey: '',
}
},
lmStudio: {
endpoint: 'http://localhost:1234',
},
liteLLM: { // https://docs.litellm.ai/docs/providers/openai_compatible
endpoint: '',
},
googleVertex: { // google https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/call-vertex-using-openai-library
region: 'us-west2',
project: '',
},
microsoftAzure: { // microsoft Azure Foundry
project: '', // really 'resource'
apiKey: '',
azureApiVersion: '2024-05-01-preview',
},
} as const
@ -84,6 +99,8 @@ export const defaultModelsOfProvider = {
],
vLLM: [ // autodetected
],
lmStudio: [], // autodetected
openRouter: [ // https://openrouter.ai/models
// 'anthropic/claude-3.7-sonnet:thinking',
'anthropic/claude-3.7-sonnet',
@ -112,6 +129,11 @@ export const defaultModelsOfProvider = {
'ministral-8b-latest',
],
openAICompatible: [], // fallback
googleVertex: [],
microsoftAzure: [],
liteLLM: [],
} as const satisfies Record<ProviderName, string[]>
@ -168,7 +190,7 @@ type VoidStaticProviderInfo = { // doesn't change (not stateful)
const modelOptionsDefaults: VoidStaticModelInfo = {
contextWindow: 32_000,
contextWindow: 16_000,
maxOutputTokens: 4_096,
cost: { input: 0, output: 0 },
downloadable: false,
@ -806,6 +828,25 @@ const groqSettings: VoidStaticProviderInfo = {
modelOptionsFallback: (modelName) => { return null }
}
// ---------------- GOOGLE VERTEX ----------------
const googleVertexModelOptions = {
} as const satisfies Record<string, VoidStaticModelInfo>
const googleVertexSettings: VoidStaticProviderInfo = {
modelOptions: googleVertexModelOptions,
modelOptionsFallback: (modelName) => { return null }
}
// ---------------- MICROSOFT AZURE ----------------
const microsoftAzureModelOptions = {
} as const satisfies Record<string, VoidStaticModelInfo>
const microsoftAzureSettings: VoidStaticProviderInfo = {
modelOptions: microsoftAzureModelOptions,
modelOptionsFallback: (modelName) => { return null }
}
// ---------------- VLLM, OLLAMA, OPENAICOMPAT (self-hosted / local) ----------------
const ollamaModelOptions = {
'qwen2.5-coder:1.5b': {
contextWindow: 32_000,
@ -858,9 +899,6 @@ const ollamaModelOptions = {
export const ollamaRecommendedModels = ['qwen2.5-coder:1.5b', 'llama3.1', 'qwq', 'deepseek-r1'] as const satisfies (keyof typeof ollamaModelOptions)[]
// ---------------- VLLM, OLLAMA, OPENAICOMPAT (self-hosted / local) ----------------
const vLLMSettings: VoidStaticProviderInfo = {
// reasoning: OAICompat + response.choices[0].delta.reasoning_content // https://docs.vllm.ai/en/stable/features/reasoning_outputs.html#streaming-chat-completions
providerReasoningIOSettings: { output: { nameOfFieldInDelta: 'reasoning_content' }, },
@ -868,6 +906,12 @@ const vLLMSettings: VoidStaticProviderInfo = {
modelOptions: {}, // TODO
}
const lmStudioSettings: VoidStaticProviderInfo = {
providerReasoningIOSettings: { output: { needsManualParse: true }, },
modelOptionsFallback: (modelName) => extensiveModelFallback(modelName, { downloadable: { sizeGb: 'not-known' } }),
modelOptions: {}, // TODO
}
const ollamaSettings: VoidStaticProviderInfo = {
// reasoning: we need to filter out reasoning <think> tags manually
providerReasoningIOSettings: { output: { needsManualParse: true }, },
@ -881,6 +925,12 @@ const openaiCompatible: VoidStaticProviderInfo = {
modelOptions: {},
}
const liteLLMSettings: VoidStaticProviderInfo = { // https://docs.litellm.ai/docs/reasoning_content
providerReasoningIOSettings: { output: { nameOfFieldInDelta: 'reasoning_content' } },
modelOptionsFallback: (modelName) => extensiveModelFallback(modelName, { downloadable: { sizeGb: 'not-known' } }),
modelOptions: {}, // TODO
}
// ---------------- OPENROUTER ----------------
const openRouterModelOptions_assumingOpenAICompat = {
@ -1027,9 +1077,12 @@ const modelSettingsOfProvider: { [providerName in ProviderName]: VoidStaticProvi
ollama: ollamaSettings,
openAICompatible: openaiCompatible,
mistral: mistralSettings,
// googleVertex: {},
// microsoftAzure: {},
// openHands: {},
liteLLM: liteLLMSettings,
lmStudio: lmStudioSettings,
googleVertex: googleVertexSettings,
microsoftAzure: microsoftAzureSettings,
} as const

View file

@ -330,6 +330,7 @@ Here's an example of a good edit suggestion:
${fileNameEditExample}.`)
}
details.push(`NEVER write the FULL PATH of a file when speaking with the user. Just write the file name ONLY.`)
details.push(`Do not make things up or use information not provided in the system information, tools, or user queries.`)
details.push(`Today's date is ${new Date().toDateString()}.`)
@ -446,9 +447,10 @@ export const DIVIDER = `=======`
export const FINAL = `>>>>>>> UPDATED`
export const searchReplace_systemMessage = `\
You are a coding assistant that generates SEARCH/REPLACE code blocks that will be used to edit a file.
You are a coding assistant that takes in a diff describing of a change to make, and outputs SEARCH/REPLACE code blocks which implement the change.
The diff will be labeled \`DIFF\` and the original file will be labeled \`ORIGINAL_FILE\`.
A SEARCH/REPLACE block describes the code before and after a change. Here is the format:
Format your SEARCH/REPLACE blocks as follows:
${tripleTick[0]}
${ORIGINAL}
// ... original code goes here
@ -457,23 +459,28 @@ ${DIVIDER}
${FINAL}
${tripleTick[1]}
You will be given the original file \`ORIGINAL_FILE\` and a diff to apply to the file, \`CHANGE\`.
Output SEARCH/REPLACE blocks to edit the file according to the desired change. You may output multiple SEARCH/REPLACE blocks.
Be sure to output a change for every single item that changed from the original file to the given change, including comments.
1. Every single item written in \`CHANGE\` should show up in the final result, except for comments explicitly saying things like "// ... existing code". Make sure to include ALL other comments (even descriptive ones), code, whitespace, etc. in the final result.
Directions:
1. Your OUTPUT should consist ONLY of SEARCH/REPLACE blocks. Do NOT output any text or explanations before or after this.
2. The "ORIGINAL" code in each SEARCH/REPLACE block must EXACTLY match lines in the original file. The original code must NOT includes any new whitespace, comments, or any other modifications from the original code.
3. The "ORIGINAL" code in each SEARCH/REPLACE block must include enough text to uniquely identify the change in the file, but please bias towards writing as little as possible.
4. The "ORIGINAL" code in each SEARCH/REPLACE block must be disjoint from all other blocks.
2. Your SEARCH/REPLACE block(s) must implement the change EXACTLY. You should use comments like "// ... existing code" as reference points, and everything else in the change should be written verbatim.
The SEARCH/REPLACE blocks you generate will be applied immediately, and so they **MUST** produce a file that the user can run IMMEDIATELY.
- Make sure you add all necessary imports.
- Make sure the "UPDATED" code is ready for production as-is, and fix any relevant lint errors.
3. You are allowed to output multiple SEARCH/REPLACE blocks.
Follow coding conventions of the user (spaces, semilcolons, comments, etc). If the user spaces or formats things a certain way, CONTINUE formatting it that way, even if you prefer otherwise.
4. Your output should consist ONLY of SEARCH/REPLACE blocks. Do NOT output any text or explanations before or after this.
5. The ORIGINAL code in each SEARCH/REPLACE block must EXACTLY match lines in the original file. Do not add or remove any whitespace, comments, or modifications from the original code.
6. Each ORIGINAL text must be large enough to uniquely identify the change in the file. However; bias towards writing as little as possible.
7. Each ORIGINAL text must be DISJOINT from all other ORIGINAL text.
## EXAMPLE 1
DIFF
${tripleTick[0]}
// ... existing code
let x = 6.5
// ... existing code
${tripleTick[1]}
ORIGINAL_FILE
${tripleTick[0]}
let w = 5
@ -482,15 +489,6 @@ let y = 7
let z = 8
${tripleTick[1]}
CHANGE
Make x equal to 6.5, not 6.
${tripleTick[0]}
// ... existing code
let x = 6.5
// ... existing code
${tripleTick[1]}
## ACCEPTED OUTPUT
${tripleTick[0]}
${ORIGINAL}
@ -502,11 +500,14 @@ ${tripleTick[1]}
`
export const searchReplace_userMessage = ({ originalCode, applyStr }: { originalCode: string, applyStr: string }) => `\
ORIGINAL_FILE
${originalCode}
DIFF
${applyStr}
CHANGE
${applyStr}`
ORIGINAL_FILE
${tripleTick[0]}
${originalCode}
${tripleTick[1]}
`

View file

@ -8,7 +8,7 @@ import { ILLMMessageService } from './sendLLMMessageService.js';
import { Emitter, Event } from '../../../../base/common/event.js';
import { Disposable, IDisposable } from '../../../../base/common/lifecycle.js';
import { RefreshableProviderName, refreshableProviderNames, SettingsOfProvider } from './voidSettingsTypes.js';
import { OllamaModelResponse, VLLMModelResponse } from './sendLLMMessageTypes.js';
import { OllamaModelResponse, OpenaiCompatibleModelResponse } from './sendLLMMessageTypes.js';
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
@ -46,6 +46,7 @@ export type RefreshModelStateOfProvider = Record<RefreshableProviderName, Refres
const refreshBasedOn: { [k in RefreshableProviderName]: (keyof SettingsOfProvider[k])[] } = {
ollama: ['_didFillInProviderSettings', 'endpoint'],
vLLM: ['_didFillInProviderSettings', 'endpoint'],
lmStudio: ['_didFillInProviderSettings', 'endpoint'],
// openAICompatible: ['_didFillInProviderSettings', 'endpoint', 'apiKey'],
}
const REFRESH_INTERVAL = 5_000
@ -142,6 +143,7 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
state: RefreshModelStateOfProvider = {
ollama: { state: 'init', timeoutId: null },
vLLM: { state: 'init', timeoutId: null },
lmStudio: { state: 'init', timeoutId: null },
}
@ -160,18 +162,18 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
}
}
const listFn = providerName === 'ollama' ? this.llmMessageService.ollamaList
: providerName === 'vLLM' ? this.llmMessageService.vLLMList
: () => { }
: this.llmMessageService.openAICompatibleList
listFn({
providerName,
onSuccess: ({ models }) => {
// set the models to the detected models
this.voidSettingsService.setAutodetectedModels(
providerName,
models.map(model => {
if (providerName === 'ollama') return (model as OllamaModelResponse).name;
else if (providerName === 'vLLM') return (model as VLLMModelResponse).id;
else if (providerName === 'vLLM') return (model as OpenaiCompatibleModelResponse).id;
else if (providerName === 'lmStudio') return (model as OpenaiCompatibleModelResponse).id;
else throw new Error('refreshMode fn: unknown provider', providerName);
}),
{ enableProviderOnSuccess: options.enableProviderOnSuccess, hideRefresh: options.doNotFire }

View file

@ -3,7 +3,7 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { EventLLMMessageOnTextParams, EventLLMMessageOnErrorParams, EventLLMMessageOnFinalMessageParams, ServiceSendLLMMessageParams, MainSendLLMMessageParams, MainLLMMessageAbortParams, ServiceModelListParams, EventModelListOnSuccessParams, EventModelListOnErrorParams, MainModelListParams, OllamaModelResponse, VLLMModelResponse, } from './sendLLMMessageTypes.js';
import { EventLLMMessageOnTextParams, EventLLMMessageOnErrorParams, EventLLMMessageOnFinalMessageParams, ServiceSendLLMMessageParams, MainSendLLMMessageParams, MainLLMMessageAbortParams, ServiceModelListParams, EventModelListOnSuccessParams, EventModelListOnErrorParams, MainModelListParams, OllamaModelResponse, OpenaiCompatibleModelResponse, } from './sendLLMMessageTypes.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
@ -22,7 +22,7 @@ export interface ILLMMessageService {
sendLLMMessage: (params: ServiceSendLLMMessageParams) => string | null;
abort: (requestId: string) => void;
ollamaList: (params: ServiceModelListParams<OllamaModelResponse>) => void;
vLLMList: (params: ServiceModelListParams<VLLMModelResponse>) => void;
openAICompatibleList: (params: ServiceModelListParams<OpenaiCompatibleModelResponse>) => void;
}
@ -46,12 +46,12 @@ export class LLMMessageService extends Disposable implements ILLMMessageService
success: {} as { [eventId: string]: ((params: EventModelListOnSuccessParams<OllamaModelResponse>) => void) },
error: {} as { [eventId: string]: ((params: EventModelListOnErrorParams<OllamaModelResponse>) => void) },
},
vLLM: {
success: {} as { [eventId: string]: ((params: EventModelListOnSuccessParams<VLLMModelResponse>) => void) },
error: {} as { [eventId: string]: ((params: EventModelListOnErrorParams<VLLMModelResponse>) => void) },
openAICompat: {
success: {} as { [eventId: string]: ((params: EventModelListOnSuccessParams<OpenaiCompatibleModelResponse>) => void) },
error: {} as { [eventId: string]: ((params: EventModelListOnErrorParams<OpenaiCompatibleModelResponse>) => void) },
}
} satisfies {
[providerName: string]: {
[providerName in 'ollama' | 'openAICompat']: {
success: { [eventId: string]: ((params: EventModelListOnSuccessParams<any>) => void) },
error: { [eventId: string]: ((params: EventModelListOnErrorParams<any>) => void) },
}
@ -70,14 +70,31 @@ export class LLMMessageService extends Disposable implements ILLMMessageService
// .listen sets up an IPC channel and takes a few ms, so we set up listeners immediately and add hooks to them instead
// llm
this._register((this.channel.listen('onText_sendLLMMessage') satisfies Event<EventLLMMessageOnTextParams>)(e => { this.llmMessageHooks.onText[e.requestId]?.(e) }))
this._register((this.channel.listen('onFinalMessage_sendLLMMessage') satisfies Event<EventLLMMessageOnFinalMessageParams>)(e => { this.llmMessageHooks.onFinalMessage[e.requestId]?.(e); this._clearChannelHooks(e.requestId) }))
this._register((this.channel.listen('onError_sendLLMMessage') satisfies Event<EventLLMMessageOnErrorParams>)(e => { this.llmMessageHooks.onError[e.requestId]?.(e); this._clearChannelHooks(e.requestId); console.error('Error in LLMMessageService:', JSON.stringify(e)) }))
// ollama .list()
this._register((this.channel.listen('onSuccess_list_ollama') satisfies Event<EventModelListOnSuccessParams<OllamaModelResponse>>)(e => { this.listHooks.ollama.success[e.requestId]?.(e) }))
this._register((this.channel.listen('onError_list_ollama') satisfies Event<EventModelListOnErrorParams<OllamaModelResponse>>)(e => { this.listHooks.ollama.error[e.requestId]?.(e) }))
this._register((this.channel.listen('onSuccess_list_vLLM') satisfies Event<EventModelListOnSuccessParams<VLLMModelResponse>>)(e => { this.listHooks.vLLM.success[e.requestId]?.(e) }))
this._register((this.channel.listen('onError_list_vLLM') satisfies Event<EventModelListOnErrorParams<VLLMModelResponse>>)(e => { this.listHooks.vLLM.error[e.requestId]?.(e) }))
this._register((this.channel.listen('onText_sendLLMMessage') satisfies Event<EventLLMMessageOnTextParams>)(e => {
this.llmMessageHooks.onText[e.requestId]?.(e)
}))
this._register((this.channel.listen('onFinalMessage_sendLLMMessage') satisfies Event<EventLLMMessageOnFinalMessageParams>)(e => {
this.llmMessageHooks.onFinalMessage[e.requestId]?.(e);
this._clearChannelHooks(e.requestId)
}))
this._register((this.channel.listen('onError_sendLLMMessage') satisfies Event<EventLLMMessageOnErrorParams>)(e => {
this.llmMessageHooks.onError[e.requestId]?.(e);
this._clearChannelHooks(e.requestId);
console.error('Error in LLMMessageService:', JSON.stringify(e))
}))
// .list()
this._register((this.channel.listen('onSuccess_list_ollama') satisfies Event<EventModelListOnSuccessParams<OllamaModelResponse>>)(e => {
this.listHooks.ollama.success[e.requestId]?.(e)
}))
this._register((this.channel.listen('onError_list_ollama') satisfies Event<EventModelListOnErrorParams<OllamaModelResponse>>)(e => {
this.listHooks.ollama.error[e.requestId]?.(e)
}))
this._register((this.channel.listen('onSuccess_list_openAICompatible') satisfies Event<EventModelListOnSuccessParams<OpenaiCompatibleModelResponse>>)(e => {
this.listHooks.openAICompat.success[e.requestId]?.(e)
}))
this._register((this.channel.listen('onError_list_openAICompatible') satisfies Event<EventModelListOnErrorParams<OpenaiCompatibleModelResponse>>)(e => {
this.listHooks.openAICompat.error[e.requestId]?.(e)
}))
}
@ -143,25 +160,24 @@ export class LLMMessageService extends Disposable implements ILLMMessageService
}
vLLMList = (params: ServiceModelListParams<VLLMModelResponse>) => {
openAICompatibleList = (params: ServiceModelListParams<OpenaiCompatibleModelResponse>) => {
const { onSuccess, onError, ...proxyParams } = params
const { settingsOfProvider } = this.voidSettingsService.state
// add state for request id
const requestId_ = generateUuid();
this.listHooks.vLLM.success[requestId_] = onSuccess
this.listHooks.vLLM.error[requestId_] = onError
this.listHooks.openAICompat.success[requestId_] = onSuccess
this.listHooks.openAICompat.error[requestId_] = onError
this.channel.call('vLLMList', {
this.channel.call('openAICompatibleList', {
...proxyParams,
settingsOfProvider,
providerName: 'vLLM',
requestId: requestId_,
} satisfies MainModelListParams<VLLMModelResponse>)
} satisfies MainModelListParams<OpenaiCompatibleModelResponse>)
}
_clearChannelHooks(requestId: string) {
private _clearChannelHooks(requestId: string) {
delete this.llmMessageHooks.onText[requestId]
delete this.llmMessageHooks.onFinalMessage[requestId]
delete this.llmMessageHooks.onError[requestId]
@ -169,8 +185,8 @@ export class LLMMessageService extends Disposable implements ILLMMessageService
delete this.listHooks.ollama.success[requestId]
delete this.listHooks.ollama.error[requestId]
delete this.listHooks.vLLM.success[requestId]
delete this.listHooks.vLLM.error[requestId]
delete this.listHooks.openAICompat.success[requestId]
delete this.listHooks.openAICompat.error[requestId]
}
}

View file

@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------*/
import { ToolName, ToolParamName } from './prompt/prompts.js'
import { ChatMode, ModelSelection, ModelSelectionOptions, ProviderName, SettingsOfProvider } from './voidSettingsTypes.js'
import { ChatMode, ModelSelection, ModelSelectionOptions, ProviderName, RefreshableProviderName, SettingsOfProvider } from './voidSettingsTypes.js'
export const errorDetails = (fullError: Error | null): string | null => {
@ -162,15 +162,13 @@ export type OllamaModelResponse = {
size_vram: number;
}
type OpenaiCompatibleModelResponse = {
export type OpenaiCompatibleModelResponse = {
id: string;
created: number;
object: 'model';
owned_by: string;
}
export type VLLMModelResponse = OpenaiCompatibleModelResponse
// params to the true list fn
@ -183,12 +181,13 @@ export type ModelListParams<ModelResponse> = {
// params to the service
export type ServiceModelListParams<modelResponse> = {
providerName: RefreshableProviderName;
onSuccess: (param: { models: modelResponse[] }) => void;
onError: (param: { error: any }) => void;
}
type BlockedMainModelListParams = 'onSuccess' | 'onError'
export type MainModelListParams<modelResponse> = Omit<ModelListParams<modelResponse>, BlockedMainModelListParams> & { requestId: string }
export type MainModelListParams<modelResponse> = Omit<ModelListParams<modelResponse>, BlockedMainModelListParams> & { providerName: RefreshableProviderName, requestId: string }
export type EventModelListOnSuccessParams<modelResponse> = Parameters<ModelListParams<modelResponse>['onSuccess']>[0] & { requestId: string }
export type EventModelListOnErrorParams<modelResponse> = Parameters<ModelListParams<modelResponse>['onError']>[0] & { requestId: string }

View file

@ -42,10 +42,15 @@ class VoidModelService extends Disposable implements IVoidModelService {
}
initializeModel = async (uri: URI) => {
if (uri.fsPath in this._modelRefOfURI) return;
const editorModelRef = await this._textModelService.createModelReference(uri);
// Keep a strong reference to prevent disposal
this._modelRefOfURI[uri.fsPath] = editorModelRef;
try {
if (uri.fsPath in this._modelRefOfURI) return;
const editorModelRef = await this._textModelService.createModelReference(uri);
// Keep a strong reference to prevent disposal
this._modelRefOfURI[uri.fsPath] = editorModelRef;
}
catch (e) {
console.log('InitializeModel error:', e)
}
};
getModelFromFsPath = (fsPath: string): VoidModelType => {

View file

@ -71,24 +71,22 @@ export interface IVoidSettingsService {
const _updatedModelsAfterDefaultModelsChange = (defaultModelNames: string[], options: { existingModels: VoidStatefulModelInfo[], didAutoDetect: boolean }) => {
const { existingModels, didAutoDetect } = options
const _modelsWithSwappedInNewModels = (options: { existingModels: VoidStatefulModelInfo[], models: string[], type: 'autodetected' | 'default' }) => {
const { existingModels, models, type } = options
const existingModelsMap: Record<string, VoidStatefulModelInfo> = {}
for (const existingModel of existingModels) {
existingModelsMap[existingModel.modelName] = existingModel
}
const newDefaultModels = defaultModelNames.map((modelName, i) => ({
modelName,
isDefault: true,
isAutodetected: didAutoDetect,
isHidden: !!existingModelsMap[modelName]?.isHidden,
}))
const newDefaultModels = models.map((modelName, i) => ({ modelName, type, isHidden: !!existingModelsMap[modelName]?.isHidden, }))
return [
...newDefaultModels, // swap out all the default models for the new default models
...existingModels.filter(m => !m.isDefault), // keep any non-default (custom) models
...newDefaultModels, // swap out all the models of this type for the new models of this type
...existingModels.filter(m => {
const keep = m.type !== type
return keep
})
]
}
@ -101,7 +99,7 @@ export const modelFilterOfFeatureName: { [featureName in FeatureName]: { filter:
}
const _stateWithUpdatedDefaultModels = (state: VoidSettingsState): VoidSettingsState => {
const _stateWithMergedDefaultModels = (state: VoidSettingsState): VoidSettingsState => {
let newSettingsOfProvider = state.settingsOfProvider
// recompute default models
@ -109,7 +107,7 @@ const _stateWithUpdatedDefaultModels = (state: VoidSettingsState): VoidSettingsS
const defaultModels = defaultSettingsOfProvider[providerName]?.models ?? []
const currentModels = newSettingsOfProvider[providerName]?.models ?? []
const defaultModelNames = defaultModels.map(m => m.modelName)
const newModels = _updatedModelsAfterDefaultModelsChange(defaultModelNames, { existingModels: currentModels, didAutoDetect: false })
const newModels = _modelsWithSwappedInNewModels({ existingModels: currentModels, models: defaultModelNames, type: 'default' })
newSettingsOfProvider = {
...newSettingsOfProvider,
[providerName]: {
@ -245,24 +243,45 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
}
// the stored data structure might be outdated, so we need to update it here
readS = {
...readS,
settingsOfProvider: {
try {
readS = {
...readS,
...defaultSettingsOfProvider,
...readS.settingsOfProvider,
mistral: { // we added mistral
...defaultSettingsOfProvider.mistral,
...readS.settingsOfProvider.mistral,
},
} // we added mistral
}
for (const providerName of providerNames) {
readS.settingsOfProvider[providerName] = {
...defaultSettingsOfProvider[providerName],
...readS.settingsOfProvider[providerName],
} as any
// conversion from 1.0.3 to 1.2.5 (can remove this when enough people update)
for (const m of readS.settingsOfProvider[providerName].models) {
if (!m.type) {
const old = (m as { isAutodetected?: boolean; isDefault?: boolean })
if (old.isAutodetected)
m.type = 'autodetected'
else if (old.isDefault)
m.type = 'default'
else m.type = 'custom'
}
}
}
}
catch (e) {
readS = defaultState()
}
this.state = readS
this.state = _stateWithUpdatedDefaultModels(this.state)
this.state = _stateWithMergedDefaultModels(this.state)
this.state = _validatedModelState(this.state);
this._resolver();
this._onDidChangeState.fire();
}
@ -389,7 +408,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
const { models } = this.state.settingsOfProvider[providerName]
const oldModelNames = models.map(m => m.modelName)
const newModels = _updatedModelsAfterDefaultModelsChange(autodetectedModelNames, { existingModels: models, didAutoDetect: true })
const newModels = _modelsWithSwappedInNewModels({ existingModels: models, models: autodetectedModelNames, type: 'autodetected' })
this.setSettingOfProvider(providerName, 'models', newModels)
// if the models changed, log it
@ -423,7 +442,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
if (existingIdx !== -1) return // if exists, do nothing
const newModels = [
...models,
{ modelName, isDefault: false, isHidden: false }
{ modelName, type: 'custom', isHidden: false } as const
]
this.setSettingOfProvider(providerName, 'models', newModels)

View file

@ -15,7 +15,7 @@ type UnionOfKeys<T> = T extends T ? keyof T : never;
export type ProviderName = keyof typeof defaultProviderSettings
export const providerNames = Object.keys(defaultProviderSettings) as ProviderName[]
export const localProviderNames = ['ollama', 'vLLM'] satisfies ProviderName[] // all local names
export const localProviderNames = ['ollama', 'vLLM', 'lmStudio'] satisfies ProviderName[] // all local names
export const nonlocalProviderNames = providerNames.filter((name) => !(localProviderNames as string[]).includes(name)) // all non-local names
type CustomSettingName = UnionOfKeys<typeof defaultProviderSettings[ProviderName]>
@ -30,9 +30,8 @@ export const customSettingNamesOfProvider = (providerName: ProviderName) => {
export type VoidStatefulModelInfo = { // <-- STATEFUL
modelName: string,
isDefault: boolean, // whether or not it's a default for its provider
type: 'default' | 'autodetected' | 'custom';
isHidden: boolean, // whether or not the user is hiding it (switched off)
isAutodetected?: boolean, // whether the model was autodetected by polling
} // TODO!!! eventually we'd want to let the user change supportsFIM, etc on the model themselves
@ -59,74 +58,78 @@ type DisplayInfoForProviderName = {
export const displayInfoOfProviderName = (providerName: ProviderName): DisplayInfoForProviderName => {
if (providerName === 'anthropic') {
return {
title: 'Anthropic',
}
return { title: 'Anthropic', }
}
else if (providerName === 'openAI') {
return {
title: 'OpenAI',
}
return { title: 'OpenAI', }
}
else if (providerName === 'deepseek') {
return {
// title: 'DeepSeek.com API',
title: 'DeepSeek',
}
return { title: 'DeepSeek', }
}
else if (providerName === 'openRouter') {
return {
title: 'OpenRouter',
}
return { title: 'OpenRouter', }
}
else if (providerName === 'ollama') {
return {
title: 'Ollama',
}
return { title: 'Ollama', }
}
else if (providerName === 'vLLM') {
return {
title: 'vLLM',
}
return { title: 'vLLM', }
}
else if (providerName === 'liteLLM') {
return { title: 'LiteLLM', }
}
else if (providerName === 'lmStudio') {
return { title: 'LM Studio', }
}
else if (providerName === 'openAICompatible') {
return {
title: 'OpenAI-Compatible',
}
return { title: 'OpenAI-Compatible', }
}
else if (providerName === 'gemini') {
return {
// title: 'Gemini API',
title: 'Gemini',
}
return { title: 'Gemini', }
}
else if (providerName === 'groq') {
return {
// title: 'Groq.com API',
title: 'Groq',
}
return { title: 'Groq', }
}
else if (providerName === 'xAI') {
return {
// title: 'Grok (xAI)',
title: 'xAI',
}
return { title: 'xAI', }
}
else if (providerName === 'mistral') {
return {
// title: 'Mistral API',
title: 'Mistral',
}
return { title: 'Mistral', }
}
else if (providerName === 'googleVertex') {
return { title: 'Google Vertex AI', }
}
else if (providerName === 'microsoftAzure') {
return { title: 'Microsoft Azure OpenAI', }
}
throw new Error(`descOfProviderName: Unknown provider name: "${providerName}"`)
}
export const subTextMdOfProviderName = (providerName: ProviderName): string => {
if (providerName === 'anthropic') return 'Get your [API Key here](https://console.anthropic.com/settings/keys).'
if (providerName === 'openAI') return 'Get your [API Key here](https://platform.openai.com/api-keys).'
if (providerName === 'deepseek') return 'Get your [API Key here](https://platform.deepseek.com/api_keys).'
if (providerName === 'openRouter') return 'Get your [API Key here](https://openrouter.ai/settings/keys).'
if (providerName === 'gemini') return 'Get your [API Key here](https://aistudio.google.com/apikey).'
if (providerName === 'groq') return 'Get your [API Key here](https://console.groq.com/keys).'
if (providerName === 'xAI') return 'Get your [API Key here](https://console.x.ai).'
if (providerName === 'mistral') return 'Get your [API Key here](https://console.mistral.ai/api-keys).'
if (providerName === 'openAICompatible') return `Use any OpenAI-compatible endpoint (LM Studio, LiteLM, etc).`
if (providerName === 'googleVertex') return 'You must authenticate before using Vertex with Void. Read more about endpoints [here](https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/call-vertex-using-openai-library), and regions [here](https://cloud.google.com/vertex-ai/docs/general/locations#available-regions).'
if (providerName === 'microsoftAzure') return 'Read more about endpoints [here](https://learn.microsoft.com/en-us/rest/api/aifoundry/model-inference/get-chat-completions/get-chat-completions?view=rest-aifoundry-model-inference-2024-05-01-preview&tabs=HTTP), and get your API key [here](https://learn.microsoft.com/en-us/azure/search/search-security-api-keys?tabs=rest-use%2Cportal-find%2Cportal-query#find-existing-keys).'
if (providerName === 'ollama') return 'If you would like to change this endpoint, please read more about [Endpoints here](https://github.com/ollama/ollama/blob/main/docs/faq.md#how-can-i-expose-ollama-on-my-network).'
if (providerName === 'vLLM') return 'If you would like to change this endpoint, please read more about [Endpoints here](https://docs.vllm.ai/en/latest/getting_started/quickstart.html#openai-compatible-server).'
if (providerName === 'lmStudio') return 'If you would like to change this endpoint, please more about [Endpoints here](https://lmstudio.ai/docs/app/api/endpoints/openai).'
if (providerName === 'liteLLM') return 'Read more about endpoints [here](https://docs.litellm.ai/docs/providers/openai_compatible).'
throw new Error(`subTextMdOfProviderName: Unknown provider name: "${providerName}"`)
}
type DisplayInfo = {
title: string;
placeholder: string;
subTextMd?: string;
isPasswordField?: boolean;
}
export const displayInfoOfSettingName = (providerName: ProviderName, settingName: SettingName): DisplayInfo => {
@ -140,23 +143,15 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
providerName === 'openAI' ? 'sk-proj-key...' :
providerName === 'deepseek' ? 'sk-key...' :
providerName === 'openRouter' ? 'sk-or-key...' : // sk-or-v1-key
providerName === 'gemini' ? 'key...' :
providerName === 'gemini' ? 'AIzaSy...' :
providerName === 'groq' ? 'gsk_key...' :
providerName === 'openAICompatible' ? 'sk-key...' :
providerName === 'xAI' ? 'xai-key...' :
providerName === 'mistral' ? 'api-key...' :
'',
providerName === 'googleVertex' ? 'AIzaSy...' :
providerName === 'microsoftAzure' ? 'key-...' :
'',
subTextMd: providerName === 'anthropic' ? 'Get your [API Key here](https://console.anthropic.com/settings/keys).' :
providerName === 'openAI' ? 'Get your [API Key here](https://platform.openai.com/api-keys).' :
providerName === 'deepseek' ? 'Get your [API Key here](https://platform.deepseek.com/api_keys).' :
providerName === 'openRouter' ? 'Get your [API Key here](https://openrouter.ai/settings/keys).' :
providerName === 'gemini' ? 'Get your [API Key here](https://aistudio.google.com/apikey).' :
providerName === 'groq' ? 'Get your [API Key here](https://console.groq.com/keys).' :
providerName === 'xAI' ? 'Get your [API Key here](https://console.x.ai).' :
providerName === 'mistral' ? 'Get your [API Key here](https://console.mistral.ai/api-keys).' :
providerName === 'openAICompatible' ? `Use any OpenAI-compatible endpoint (LM Studio, LiteLM, etc).` :
'',
isPasswordField: true,
}
}
@ -164,19 +159,51 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
return {
title: providerName === 'ollama' ? 'Endpoint' :
providerName === 'vLLM' ? 'Endpoint' :
providerName === 'openAICompatible' ? 'baseURL' : // (do not include /chat/completions)
'(never)',
providerName === 'lmStudio' ? 'Endpoint' :
providerName === 'openAICompatible' ? 'baseURL' : // (do not include /chat/completions)
providerName === 'googleVertex' ? 'baseURL' :
providerName === 'microsoftAzure' ? 'baseURL' :
providerName === 'liteLLM' ? 'baseURL' :
'(never)',
placeholder: providerName === 'ollama' ? defaultProviderSettings.ollama.endpoint
: providerName === 'vLLM' ? defaultProviderSettings.vLLM.endpoint
: providerName === 'openAICompatible' ? 'https://my-website.com/v1'
: '(never)',
: providerName === 'lmStudio' ? defaultProviderSettings.lmStudio.endpoint
: providerName === 'liteLLM' ? 'http://localhost:4000'
: '(never)',
subTextMd: providerName === 'ollama' ? 'If you would like to change this endpoint, please read more about [Endpoints here](https://github.com/ollama/ollama/blob/main/docs/faq.md#how-can-i-expose-ollama-on-my-network).' :
providerName === 'vLLM' ? 'If you would like to change this endpoint, please read more about [Endpoints here](https://docs.vllm.ai/en/latest/getting_started/quickstart.html#openai-compatible-server).' :
undefined,
}
}
else if (settingName === 'region') {
// vertex only
return {
title: 'Region',
placeholder: providerName === 'googleVertex' ? defaultProviderSettings.googleVertex.region
: ''
}
}
else if (settingName === 'azureApiVersion') {
// azure only
return {
title: 'API Version',
placeholder: providerName === 'microsoftAzure' ? defaultProviderSettings.microsoftAzure.azureApiVersion
: ''
}
}
else if (settingName === 'project') {
return {
title: providerName === 'googleVertex' ? 'Project'
: providerName === 'microsoftAzure' ? 'Resource'
: '',
placeholder: providerName === 'googleVertex' ? 'my-project'
: providerName === 'microsoftAzure' ? 'my-resource'
: ''
}
}
else if (settingName === '_didFillInProviderSettings') {
return {
title: '(never)',
@ -200,6 +227,9 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
const defaultCustomSettings: Record<CustomSettingName, undefined> = {
apiKey: undefined,
endpoint: undefined,
region: undefined,
project: undefined,
azureApiVersion: undefined,
}
@ -207,8 +237,7 @@ const modelInfoOfDefaultModelNames = (defaultModelNames: string[]): { models: Vo
return {
models: defaultModelNames.map((modelName, i) => ({
modelName,
isDefault: true,
isAutodetected: false,
type: 'default',
isHidden: defaultModelNames.length >= 10, // hide all models if there are a ton of them, and make user enable them individually
}))
}
@ -252,6 +281,18 @@ export const defaultSettingsOfProvider: SettingsOfProvider = {
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.mistral),
_didFillInProviderSettings: undefined,
},
liteLLM: {
...defaultCustomSettings,
...defaultProviderSettings.liteLLM,
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.liteLLM),
_didFillInProviderSettings: undefined,
},
lmStudio: {
...defaultCustomSettings,
...defaultProviderSettings.lmStudio,
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.lmStudio),
_didFillInProviderSettings: undefined,
},
groq: { // aggregator (serves models from multiple providers)
...defaultCustomSettings,
...defaultProviderSettings.groq,
@ -282,6 +323,18 @@ export const defaultSettingsOfProvider: SettingsOfProvider = {
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.vLLM),
_didFillInProviderSettings: undefined,
},
googleVertex: { // aggregator (serves models from multiple providers)
...defaultCustomSettings,
...defaultProviderSettings.googleVertex,
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.googleVertex),
_didFillInProviderSettings: undefined,
},
microsoftAzure: { // aggregator (serves models from multiple providers)
...defaultCustomSettings,
...defaultProviderSettings.microsoftAzure,
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.microsoftAzure),
_didFillInProviderSettings: undefined,
},
}

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 { GoogleAuth } from 'google-auth-library'
/* eslint-enable */
import { AnthropicLLMChatMessage, LLMChatMessage, LLMFIMMessage, ModelListParams, OllamaModelResponse, OnError, OnFinalMessage, OnText, RawToolCallObj, RawToolParamsObj } from '../../common/sendLLMMessageTypes.js';
@ -19,6 +20,8 @@ import { extractReasoningWrapper, extractXMLToolsWrapper } from './extractGramma
import { availableTools, InternalToolInfo, isAToolName, ToolParamName, voidTools } from '../../common/prompt/prompts.js';
type InternalCommonMessageParams = {
onText: OnText;
onFinalMessage: OnFinalMessage;
@ -39,7 +42,16 @@ const invalidApiKeyMessage = (providerName: ProviderName) => `Invalid ${displayI
// ------------ OPENAI-COMPATIBLE (HELPERS) ------------
const newOpenAICompatibleSDK = ({ settingsOfProvider, providerName, includeInPayload }: { settingsOfProvider: SettingsOfProvider, providerName: ProviderName, includeInPayload?: { [s: string]: any } }) => {
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 } }) => {
const commonPayloadOpts: ClientOptions = {
dangerouslyAllowBrowser: true,
...includeInPayload,
@ -56,6 +68,14 @@ const newOpenAICompatibleSDK = ({ settingsOfProvider, providerName, includeInPay
const thisConfig = settingsOfProvider[providerName]
return new OpenAI({ baseURL: `${thisConfig.endpoint}/v1`, apiKey: 'noop', ...commonPayloadOpts })
}
else if (providerName === 'liteLLM') {
const thisConfig = settingsOfProvider[providerName]
return new OpenAI({ baseURL: `${thisConfig.endpoint}/v1`, apiKey: 'noop', ...commonPayloadOpts })
}
else if (providerName === 'lmStudio') {
const thisConfig = settingsOfProvider[providerName]
return new OpenAI({ baseURL: `${thisConfig.endpoint}/v1`, apiKey: 'noop', ...commonPayloadOpts })
}
else if (providerName === 'openRouter') {
const thisConfig = settingsOfProvider[providerName]
return new OpenAI({
@ -70,8 +90,22 @@ const newOpenAICompatibleSDK = ({ settingsOfProvider, providerName, includeInPay
}
else if (providerName === 'gemini') {
const thisConfig = settingsOfProvider[providerName]
return new OpenAI({ baseURL: 'https://generativelanguage.googleapis.com/v1beta/openai/', apiKey: thisConfig.apiKey, ...commonPayloadOpts })
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 apiKey = await getGoogleApiKey()
const thisConfig = settingsOfProvider[providerName]
const baseURL = `https://${thisConfig.region}-aiplatform.googleapis.com/v1/projects/${thisConfig.project}/locations/${thisConfig.region}/endpoints/${'openapi'}`
return new OpenAI({ baseURL: baseURL, apiKey: apiKey, ...commonPayloadOpts })
}
else if (providerName === 'microsoftAzure') {
// https://learn.microsoft.com/en-us/rest/api/aifoundry/model-inference/get-chat-completions/get-chat-completions?view=rest-aifoundry-model-inference-2024-05-01-preview&tabs=HTTP
const thisConfig = settingsOfProvider[providerName]
const baseURL = `https://${thisConfig.project}.services.ai.azure.com/api/models/chat/completions??api-version=${thisConfig.azureApiVersion}`
return new OpenAI({ baseURL: baseURL, apiKey: thisConfig.apiKey, ...commonPayloadOpts })
}
else if (providerName === 'deepseek') {
const thisConfig = settingsOfProvider[providerName]
return new OpenAI({ baseURL: 'https://api.deepseek.com/v1', apiKey: thisConfig.apiKey, ...commonPayloadOpts })
@ -97,7 +131,7 @@ const newOpenAICompatibleSDK = ({ settingsOfProvider, providerName, includeInPay
}
const _sendOpenAICompatibleFIM = ({ messages: { prefix, suffix, stopTokens }, onFinalMessage, onError, settingsOfProvider, modelName: modelName_, _setAborter, providerName, }: SendFIMParams_Internal) => {
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_)
@ -107,7 +141,7 @@ const _sendOpenAICompatibleFIM = ({ messages: { prefix, suffix, stopTokens }, on
return
}
const openai = newOpenAICompatibleSDK({ providerName, settingsOfProvider })
const openai = await newOpenAICompatibleSDK({ providerName, settingsOfProvider })
openai.completions
.create({
model: modelName,
@ -178,7 +212,7 @@ const openAIToolToRawToolCallObj = (name: string, toolParamsStr: string, id: str
// ------------ OPENAI-COMPATIBLE ------------
const _sendOpenAICompatibleChat = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName: modelName_, _setAborter, providerName, chatMode, separateSystemMessage }: SendChatParams_Internal) => {
const _sendOpenAICompatibleChat = async ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName: modelName_, _setAborter, providerName, chatMode, separateSystemMessage }: SendChatParams_Internal) => {
const {
modelName,
specialToolFormat,
@ -199,7 +233,7 @@ const _sendOpenAICompatibleChat = ({ messages, onText, onFinalMessage, onError,
: {}
// instance
const openai: OpenAI = newOpenAICompatibleSDK({ providerName, settingsOfProvider, includeInPayload })
const openai: OpenAI = await newOpenAICompatibleSDK({ providerName, settingsOfProvider, includeInPayload })
const options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = {
model: modelName,
messages: messages as any,
@ -300,7 +334,7 @@ const _openaiCompatibleList = async ({ onSuccess: onSuccess_, onError: onError_,
onError_({ error })
}
try {
const openai = newOpenAICompatibleSDK({ providerName, settingsOfProvider })
const openai = await newOpenAICompatibleSDK({ providerName, settingsOfProvider })
openai.models.list()
.then(async (response) => {
const models: OpenAIModel[] = []
@ -360,7 +394,7 @@ const anthropicToolToRawToolCallObj = (toolBlock: Anthropic.Messages.ToolUseBloc
}
// ------------ ANTHROPIC ------------
const sendAnthropicChat = ({ messages, providerName, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName: modelName_, _setAborter, separateSystemMessage, chatMode }: SendChatParams_Internal) => {
const sendAnthropicChat = async ({ messages, providerName, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName: modelName_, _setAborter, separateSystemMessage, chatMode }: SendChatParams_Internal) => {
const {
modelName,
specialToolFormat,
@ -505,6 +539,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
: content.map(chunk => (chunk.type === 'text' ? chunk.text : '')).join('')
@ -584,7 +619,7 @@ const sendOllamaFIM = ({ messages, onFinalMessage, onError, settingsOfProvider,
type CallFnOfProvider = {
[providerName in ProviderName]: {
sendChat: (params: SendChatParams_Internal) => void;
sendChat: (params: SendChatParams_Internal) => Promise<void>;
sendFIM: ((params: SendFIMParams_Internal) => void) | null;
list: ((params: ListParams_Internal<any>) => void) | null;
}
@ -646,6 +681,27 @@ export const sendLLMMessageToProviderImplementation = {
sendFIM: null,
list: null,
},
lmStudio: {
sendChat: (params) => _sendOpenAICompatibleChat(params),
sendFIM: null, // lmStudio has no suffix parameter in /completions
list: (params) => _openaiCompatibleList(params),
},
liteLLM: {
sendChat: (params) => _sendOpenAICompatibleChat(params),
sendFIM: null,
list: null,
},
googleVertex: {
sendChat: (params) => _sendOpenAICompatibleChat(params),
sendFIM: null,
list: null,
},
microsoftAzure: {
sendChat: (params) => _sendOpenAICompatibleChat(params),
sendFIM: null,
list: null,
},
} satisfies CallFnOfProvider

View file

@ -9,7 +9,7 @@ import { displayInfoOfProviderName } from '../../common/voidSettingsTypes.js';
import { sendLLMMessageToProviderImplementation } from './sendLLMMessage.impl.js';
export const sendLLMMessage = ({
export const sendLLMMessage = async ({
messagesType,
messages: messages_,
onText: onText_,
@ -108,12 +108,12 @@ export const sendLLMMessage = ({
}
const { sendFIM, sendChat } = implementation
if (messagesType === 'chatMessages') {
sendChat({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName, _setAborter, providerName, separateSystemMessage, chatMode })
await sendChat({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName, _setAborter, providerName, separateSystemMessage, chatMode })
return
}
if (messagesType === 'FIMMessage') {
if (sendFIM) {
sendFIM({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName, _setAborter, providerName, separateSystemMessage })
await sendFIM({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName, _setAborter, providerName, separateSystemMessage })
return
}
onError({ message: `Error: This provider does not support Autocomplete yet.`, fullError: null })

View file

@ -8,7 +8,7 @@
import { IServerChannel } from '../../../../base/parts/ipc/common/ipc.js';
import { Emitter, Event } from '../../../../base/common/event.js';
import { EventLLMMessageOnTextParams, EventLLMMessageOnErrorParams, EventLLMMessageOnFinalMessageParams, MainSendLLMMessageParams, AbortRef, SendLLMMessageParams, MainLLMMessageAbortParams, ModelListParams, EventModelListOnSuccessParams, EventModelListOnErrorParams, OllamaModelResponse, VLLMModelResponse, MainModelListParams, } from '../common/sendLLMMessageTypes.js';
import { EventLLMMessageOnTextParams, EventLLMMessageOnErrorParams, EventLLMMessageOnFinalMessageParams, MainSendLLMMessageParams, AbortRef, SendLLMMessageParams, MainLLMMessageAbortParams, ModelListParams, EventModelListOnSuccessParams, EventModelListOnErrorParams, OllamaModelResponse, OpenaiCompatibleModelResponse, MainModelListParams, } from '../common/sendLLMMessageTypes.js';
import { sendLLMMessage } from './llmMessage/sendLLMMessage.js'
import { IMetricsService } from '../common/metricsService.js';
import { sendLLMMessageToProviderImplementation } from './llmMessage/sendLLMMessage.impl.js';
@ -25,7 +25,7 @@ export class LLMMessageChannel implements IServerChannel {
}
// aborters for above
private readonly abortRefOfRequestId: Record<string, AbortRef> = {}
private readonly _infoOfRunningRequest: Record<string, { waitForSend: Promise<void> | undefined, abortRef: AbortRef }> = {}
// list
@ -34,12 +34,12 @@ export class LLMMessageChannel implements IServerChannel {
success: new Emitter<EventModelListOnSuccessParams<OllamaModelResponse>>(),
error: new Emitter<EventModelListOnErrorParams<OllamaModelResponse>>(),
},
vLLM: {
success: new Emitter<EventModelListOnSuccessParams<VLLMModelResponse>>(),
error: new Emitter<EventModelListOnErrorParams<VLLMModelResponse>>(),
}
openaiCompat: {
success: new Emitter<EventModelListOnSuccessParams<OpenaiCompatibleModelResponse>>(),
error: new Emitter<EventModelListOnErrorParams<OpenaiCompatibleModelResponse>>(),
},
} satisfies {
[providerName: string]: {
[providerName in 'ollama' | 'openaiCompat']: {
success: Emitter<EventModelListOnSuccessParams<any>>,
error: Emitter<EventModelListOnErrorParams<any>>,
}
@ -59,8 +59,8 @@ export class LLMMessageChannel implements IServerChannel {
// list
else if (event === 'onSuccess_list_ollama') return this.listEmitters.ollama.success.event;
else if (event === 'onError_list_ollama') return this.listEmitters.ollama.error.event;
else if (event === 'onSuccess_list_vLLM') return this.listEmitters.vLLM.success.event;
else if (event === 'onError_list_vLLM') return this.listEmitters.vLLM.error.event;
else if (event === 'onSuccess_list_openAICompatible') return this.listEmitters.openaiCompat.success.event;
else if (event === 'onError_list_openAICompatible') return this.listEmitters.openaiCompat.error.event;
else throw new Error(`Event not found: ${event}`);
}
@ -72,13 +72,13 @@ export class LLMMessageChannel implements IServerChannel {
this._callSendLLMMessage(params)
}
else if (command === 'abort') {
this._callAbort(params)
await this._callAbort(params)
}
else if (command === 'ollamaList') {
this._callOllamaList(params)
}
else if (command === 'vLLMList') {
this._callVLLMList(params)
else if (command === 'openAICompatibleList') {
this._callOpenAICompatibleList(params)
}
else {
throw new Error(`Void sendLLM: command "${command}" not recognized.`)
@ -90,27 +90,37 @@ export class LLMMessageChannel implements IServerChannel {
}
// the only place sendLLMMessage is actually called
private async _callSendLLMMessage(params: MainSendLLMMessageParams) {
private _callSendLLMMessage(params: MainSendLLMMessageParams) {
const { requestId } = params;
if (!(requestId in this.abortRefOfRequestId))
this.abortRefOfRequestId[requestId] = { current: null }
if (!(requestId in this._infoOfRunningRequest))
this._infoOfRunningRequest[requestId] = { waitForSend: undefined, abortRef: { current: null } }
const mainThreadParams: SendLLMMessageParams = {
...params,
onText: (p) => { this.llmMessageEmitters.onText.fire({ requestId, ...p }); },
onFinalMessage: (p) => { this.llmMessageEmitters.onFinalMessage.fire({ requestId, ...p }); },
onError: (p) => { console.log('sendLLM: firing err'); this.llmMessageEmitters.onError.fire({ requestId, ...p }); },
abortRef: this.abortRefOfRequestId[requestId],
onText: (p) => {
this.llmMessageEmitters.onText.fire({ requestId, ...p });
},
onFinalMessage: (p) => {
this.llmMessageEmitters.onFinalMessage.fire({ requestId, ...p });
},
onError: (p) => {
console.log('sendLLM: firing err');
this.llmMessageEmitters.onError.fire({ requestId, ...p });
},
abortRef: this._infoOfRunningRequest[requestId].abortRef,
}
sendLLMMessage(mainThreadParams, this.metricsService);
const p = sendLLMMessage(mainThreadParams, this.metricsService);
this._infoOfRunningRequest[requestId].waitForSend = p
}
private _callAbort(params: MainLLMMessageAbortParams) {
private async _callAbort(params: MainLLMMessageAbortParams) {
const { requestId } = params;
if (!(requestId in this.abortRefOfRequestId)) return
this.abortRefOfRequestId[requestId].current?.()
delete this.abortRefOfRequestId[requestId]
if (!(requestId in this._infoOfRunningRequest)) return
const { waitForSend, abortRef } = this._infoOfRunningRequest[requestId]
await waitForSend // wait for the send to finish so we know abortRef was set
abortRef?.current?.()
delete this._infoOfRunningRequest[requestId]
}
@ -128,15 +138,15 @@ export class LLMMessageChannel implements IServerChannel {
sendLLMMessageToProviderImplementation.ollama.list(mainThreadParams)
}
_callVLLMList = (params: MainModelListParams<VLLMModelResponse>) => {
const { requestId } = params
const emitters = this.listEmitters.vLLM
const mainThreadParams: ModelListParams<VLLMModelResponse> = {
_callOpenAICompatibleList = (params: MainModelListParams<OpenaiCompatibleModelResponse>) => {
const { requestId, providerName } = params
const emitters = this.listEmitters.openaiCompat
const mainThreadParams: ModelListParams<OpenaiCompatibleModelResponse> = {
...params,
onSuccess: (p) => { emitters.success.fire({ requestId, ...p }); },
onError: (p) => { emitters.error.fire({ requestId, ...p }); },
}
sendLLMMessageToProviderImplementation.vLLM.list(mainThreadParams)
sendLLMMessageToProviderImplementation[providerName].list(mainThreadParams)
}