mirror of
https://github.com/voideditor/void
synced 2026-05-23 09:28:23 +00:00
Merge pull request #294 from voideditor/model-selection
Provider Settings + UX improvements
This commit is contained in:
commit
75f30bf7cd
37 changed files with 2939 additions and 2093 deletions
13
package-lock.json
generated
13
package-lock.json
generated
|
|
@ -57,7 +57,7 @@
|
|||
"node-pty": "1.1.0-beta21",
|
||||
"ollama": "^0.5.11",
|
||||
"open": "^8.4.2",
|
||||
"openai": "^4.76.1",
|
||||
"openai": "^4.85.4",
|
||||
"posthog-node": "^4.3.1",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
|
|
@ -17079,9 +17079,10 @@
|
|||
}
|
||||
},
|
||||
"node_modules/openai": {
|
||||
"version": "4.77.0",
|
||||
"resolved": "https://registry.npmjs.org/openai/-/openai-4.77.0.tgz",
|
||||
"integrity": "sha512-WWacavtns/7pCUkOWvQIjyOfcdr9X+9n9Vvb0zFeKVDAqwCMDHB+iSr24SVaBAhplvSG6JrRXFpcNM9gWhOGIw==",
|
||||
"version": "4.85.4",
|
||||
"resolved": "https://registry.npmjs.org/openai/-/openai-4.85.4.tgz",
|
||||
"integrity": "sha512-Nki51PBSu+Aryo7WKbdXvfm0X/iKkQS2fq3O0Uqb/O3b4exOZFid2te1BZ52bbO5UwxQZ5eeHJDCTqtrJLPw0w==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/node-fetch": "^2.6.4",
|
||||
|
|
@ -17095,9 +17096,13 @@
|
|||
"openai": "bin/cli"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"ws": "^8.18.0",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"ws": {
|
||||
"optional": true
|
||||
},
|
||||
"zod": {
|
||||
"optional": true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@
|
|||
"node-pty": "1.1.0-beta21",
|
||||
"ollama": "^0.5.11",
|
||||
"open": "^8.4.2",
|
||||
"openai": "^4.76.1",
|
||||
"openai": "^4.85.4",
|
||||
"posthog-node": "^4.3.1",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
|
|
|
|||
|
|
@ -270,7 +270,7 @@ export class EditorGroupWatermark extends Disposable {
|
|||
|
||||
const keys3 = this.keybindingService.lookupKeybinding('workbench.action.openGlobalKeybindings');
|
||||
const button3 = append(recentsBox, $('button'));
|
||||
button3.textContent = 'Void Settings'
|
||||
button3.textContent = `Void Settings`
|
||||
button3.style.display = 'block'
|
||||
button3.style.marginLeft = 'auto'
|
||||
button3.style.marginRight = 'auto'
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import { ITextModelService } from '../../../../editor/common/services/resolverSe
|
|||
import { Range } from '../../../../editor/common/core/range.js';
|
||||
import { CancellationToken } from '../../../../base/common/cancellation.js';
|
||||
import { CodeActionContext, CodeActionTriggerType } from '../../../../editor/common/languages.js';
|
||||
import { URI } from '../../../../base/common/uri.js';
|
||||
|
||||
export interface IMarkerCheckService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
|
@ -99,6 +100,21 @@ class MarkerCheckService extends Disposable implements IMarkerCheckService {
|
|||
}
|
||||
|
||||
|
||||
|
||||
|
||||
fixErrorsInFiles(uris: URI[], contextSoFar: []) {
|
||||
// const allMarkers = this._markerService.read();
|
||||
|
||||
|
||||
// check errors in files
|
||||
|
||||
|
||||
// give LLM errors in files
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
// private _onMarkersChanged = (changedResources: readonly URI[]): void => {
|
||||
// for (const resource of changedResources) {
|
||||
// const markers = this._markerService.read({ resource });
|
||||
|
|
|
|||
|
|
@ -16,9 +16,9 @@ import { isCodeEditor } from '../../../../editor/browser/editorBrowser.js';
|
|||
import { EditorResourceAccessor } from '../../../common/editor.js';
|
||||
import { IModelService } from '../../../../editor/common/services/model.js';
|
||||
import { extractCodeFromRegular } from './helpers/extractCodeFromResult.js';
|
||||
import { isWindows } from '../../../../base/common/platform.js';
|
||||
import { registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js';
|
||||
import { ILLMMessageService } from '../common/llmMessageService.js';
|
||||
import { _ln, allLinebreakSymbols } from '../common/voidFileService.js';
|
||||
// import { IContextGatheringService } from './contextGatheringService.js';
|
||||
|
||||
// The extension this was called from is here - https://github.com/voideditor/void/blob/autocomplete/extensions/void/src/extension/extension.ts
|
||||
|
|
@ -415,9 +415,6 @@ const toInlineCompletions = ({ autocompletionMatchup, autocompletion, prefixAndS
|
|||
// }
|
||||
|
||||
|
||||
const allLinebreakSymbols = ['\r\n', '\n']
|
||||
const _ln = isWindows ? allLinebreakSymbols[0] : allLinebreakSymbols[1]
|
||||
|
||||
type PrefixAndSuffixInfo = { prefix: string, suffix: string, prefixLines: string[], suffixLines: string[], prefixToTheLeftOfCursor: string, suffixToTheRightOfCursor: string }
|
||||
const getPrefixAndSuffixInfo = (model: ITextModel, position: Position): PrefixAndSuffixInfo => {
|
||||
|
||||
|
|
@ -798,26 +795,27 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
|
|||
},
|
||||
useProviderFor: 'Autocomplete',
|
||||
logging: { loggingName: 'Autocomplete' },
|
||||
onText: async ({ fullText, newText }) => {
|
||||
onText: () => { }, // unused in FIMMessage
|
||||
// onText: async ({ fullText, newText }) => {
|
||||
|
||||
newAutocompletion.insertText = fullText
|
||||
// newAutocompletion.insertText = fullText
|
||||
|
||||
// count newlines in newText
|
||||
const numNewlines = newText.match(/\n|\r\n/g)?.length || 0
|
||||
newAutocompletion._newlineCount += numNewlines
|
||||
// // count newlines in newText
|
||||
// const numNewlines = newText.match(/\n|\r\n/g)?.length || 0
|
||||
// newAutocompletion._newlineCount += numNewlines
|
||||
|
||||
// if too many newlines, resolve up to last newline
|
||||
if (newAutocompletion._newlineCount > 10) {
|
||||
const lastNewlinePos = fullText.lastIndexOf('\n')
|
||||
newAutocompletion.insertText = fullText.substring(0, lastNewlinePos)
|
||||
resolve(newAutocompletion.insertText)
|
||||
return
|
||||
}
|
||||
// // if too many newlines, resolve up to last newline
|
||||
// if (newAutocompletion._newlineCount > 10) {
|
||||
// const lastNewlinePos = fullText.lastIndexOf('\n')
|
||||
// newAutocompletion.insertText = fullText.substring(0, lastNewlinePos)
|
||||
// resolve(newAutocompletion.insertText)
|
||||
// return
|
||||
// }
|
||||
|
||||
// if (!getAutocompletionMatchup({ prefix: this._lastPrefix, autocompletion: newAutocompletion })) {
|
||||
// reject('LLM response did not match user\'s text.')
|
||||
// }
|
||||
},
|
||||
// // if (!getAutocompletionMatchup({ prefix: this._lastPrefix, autocompletion: newAutocompletion })) {
|
||||
// // reject('LLM response did not match user\'s text.')
|
||||
// // }
|
||||
// },
|
||||
onFinalMessage: ({ fullText }) => {
|
||||
|
||||
// console.log('____res: ', JSON.stringify(newAutocompletion.insertText))
|
||||
|
|
|
|||
|
|
@ -12,12 +12,12 @@ import { URI } from '../../../../base/common/uri.js';
|
|||
import { Emitter, Event } from '../../../../base/common/event.js';
|
||||
import { IRange } from '../../../../editor/common/core/range.js';
|
||||
import { ILLMMessageService } from '../common/llmMessageService.js';
|
||||
import { IModelService } from '../../../../editor/common/services/model.js';
|
||||
import { chat_userMessageContent, chat_systemMessage, chat_userMessageContentWithAllFilesToo as chat_userMessageContentWithAllFiles, chat_selectionsString } from './prompt/prompts.js';
|
||||
import { IFileService } from '../../../../platform/files/common/files.js';
|
||||
import { InternalToolInfo, IToolsService, ToolCallReturnType, ToolFns, ToolName, voidTools } from '../common/toolsService.js';
|
||||
import { toLLMChatMessage } from '../common/llmMessageTypes.js';
|
||||
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
|
||||
import { IVoidFileService } from '../common/voidFileService.js';
|
||||
import { generateUuid } from '../../../../base/common/uuid.js';
|
||||
|
||||
|
||||
const findLastIndex = <T>(arr: T[], condition: (t: T) => boolean): number => {
|
||||
|
|
@ -48,13 +48,13 @@ export type FileSelection = {
|
|||
export type StagingSelectionItem = CodeSelection | FileSelection
|
||||
|
||||
|
||||
type ToolMessage<T extends ToolName> = {
|
||||
export type ToolMessage<T extends ToolName> = {
|
||||
role: 'tool';
|
||||
name: T; // internal use
|
||||
params: string; // internal use
|
||||
id: string; // apis require this tool use id
|
||||
content: string; // result
|
||||
result: ToolCallReturnType<T>; // text message of result
|
||||
result: ToolCallReturnType[T]; // text message of result
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -73,8 +73,7 @@ export type ChatMessage =
|
|||
stagingSelections: StagingSelectionItem[];
|
||||
isBeingEdited: boolean;
|
||||
}
|
||||
}
|
||||
| {
|
||||
} | {
|
||||
role: 'assistant';
|
||||
content: string | null; // content received from LLM - allowed to be '', will be replaced with (empty)
|
||||
displayContent: string | null; // content displayed to user (this is the same as content for now) - allowed to be '', will be ignored
|
||||
|
|
@ -86,7 +85,7 @@ type UserMessageState = UserMessageType['state']
|
|||
|
||||
export const defaultMessageState: UserMessageState = {
|
||||
stagingSelections: [],
|
||||
isBeingEdited: false
|
||||
isBeingEdited: false,
|
||||
}
|
||||
|
||||
// a 'thread' means a chat message history
|
||||
|
|
@ -125,7 +124,7 @@ export type ThreadStreamState = {
|
|||
const newThreadObject = () => {
|
||||
const now = new Date().toISOString()
|
||||
return {
|
||||
id: new Date().getTime().toString(),
|
||||
id: generateUuid(),
|
||||
createdAt: now,
|
||||
lastModified: now,
|
||||
messages: [],
|
||||
|
|
@ -158,16 +157,25 @@ export interface IChatThreadService {
|
|||
openNewThread(): void;
|
||||
switchToThread(threadId: string): void;
|
||||
|
||||
// you can edit multiple messages
|
||||
// the one you're currently editing is "focused", and we add items to that one when you press cmd+L.
|
||||
getFocusedMessageIdx(): number | undefined;
|
||||
isFocusingMessage(): boolean;
|
||||
setFocusedMessageIdx(messageIdx: number | undefined): void;
|
||||
|
||||
// _useFocusedStagingState(messageIdx?: number | undefined): readonly [StagingInfo, (stagingInfo: StagingInfo) => void];
|
||||
_useCurrentThreadState(): readonly [ThreadType['state'], (newState: Partial<ThreadType['state']>) => void];
|
||||
_useCurrentMessageState(messageIdx: number): readonly [UserMessageState, (newState: Partial<UserMessageState>) => void];
|
||||
// exposed getters/setters
|
||||
getCurrentMessageState: (messageIdx: number) => UserMessageState
|
||||
setCurrentMessageState: (messageIdx: number, newState: Partial<UserMessageState>) => void
|
||||
getCurrentThreadStagingSelections: () => StagingSelectionItem[]
|
||||
setCurrentThreadStagingSelections: (stagingSelections: StagingSelectionItem[]) => void
|
||||
|
||||
|
||||
// call to edit a message
|
||||
editUserMessageAndStreamResponse({ userMessage, chatMode, messageIdx }: { userMessage: string, chatMode: ChatMode, messageIdx: number }): Promise<void>;
|
||||
|
||||
// call to add a message
|
||||
addUserMessageAndStreamResponse({ userMessage, chatMode }: { userMessage: string, chatMode: ChatMode }): Promise<void>;
|
||||
|
||||
cancelStreaming(threadId: string): void;
|
||||
dismissStreamError(threadId: string): void;
|
||||
|
||||
|
|
@ -189,8 +197,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
|
||||
constructor(
|
||||
@IStorageService private readonly _storageService: IStorageService,
|
||||
@IModelService private readonly _modelService: IModelService,
|
||||
@IFileService private readonly _fileService: IFileService,
|
||||
@IVoidFileService private readonly _voidFileService: IVoidFileService,
|
||||
@ILLMMessageService private readonly _llmMessageService: ILLMMessageService,
|
||||
@IToolsService private readonly _toolsService: IToolsService,
|
||||
@IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService,
|
||||
|
|
@ -358,7 +365,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
// add user's message to chat history
|
||||
const instructions = userMessage
|
||||
const userMessageContent = await chat_userMessageContent(instructions, currSelns)
|
||||
const selectionsStr = await chat_selectionsString(prevSelns, currSelns, this._modelService, this._fileService)
|
||||
const selectionsStr = await chat_selectionsString(prevSelns, currSelns, this._voidFileService)
|
||||
const userMessageFullContent = chat_userMessageContentWithAllFiles(userMessageContent, selectionsStr)
|
||||
|
||||
const userHistoryElt: ChatMessage = { role: 'user', content: userMessageContent, displayContent: instructions, selections: currSelns, state: defaultMessageState }
|
||||
|
|
@ -423,10 +430,10 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
|
||||
// 1.
|
||||
let toolResult: Awaited<ReturnType<ToolFns[ToolName]>>
|
||||
let toolResultVal: ToolCallReturnType<ToolName>
|
||||
let toolResultVal: ToolCallReturnType[ToolName]
|
||||
try {
|
||||
toolResult = await this._toolsService.toolFns[toolName](tool.params)
|
||||
toolResultVal = toolResult[0]
|
||||
toolResultVal = toolResult
|
||||
} catch (error) {
|
||||
this._setStreamState(threadId, { error })
|
||||
shouldSendAnotherMessage = false
|
||||
|
|
@ -619,33 +626,27 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
|
||||
}
|
||||
|
||||
getCurrentThreadStagingSelections = () => {
|
||||
return this.getCurrentThread().state.stagingSelections
|
||||
}
|
||||
|
||||
setCurrentThreadStagingSelections = (stagingSelections: StagingSelectionItem[]) => {
|
||||
this._setCurrentThreadState({ stagingSelections })
|
||||
}
|
||||
|
||||
// gets `staging` and `setStaging` of the currently focused element, given the index of the currently selected message (or undefined if no message is selected)
|
||||
|
||||
_useCurrentMessageState(messageIdx: number) {
|
||||
|
||||
const thread = this.getCurrentThread()
|
||||
const messages = thread.messages
|
||||
const currMessage = messages[messageIdx]
|
||||
|
||||
if (currMessage.role !== 'user') {
|
||||
return [defaultMessageState, (s: any) => { }] as const
|
||||
}
|
||||
|
||||
const state = currMessage.state
|
||||
const setState = (newState: Partial<UserMessageState>) => this._setCurrentMessageState(newState, messageIdx)
|
||||
|
||||
return [state, setState] as const
|
||||
|
||||
getCurrentMessageState(messageIdx: number): UserMessageState {
|
||||
const currMessage = this.getCurrentThread()?.messages?.[messageIdx]
|
||||
if (!currMessage || currMessage.role !== 'user') return defaultMessageState
|
||||
return currMessage.state
|
||||
}
|
||||
setCurrentMessageState(messageIdx: number, newState: Partial<UserMessageState>) {
|
||||
const currMessage = this.getCurrentThread()?.messages?.[messageIdx]
|
||||
if (!currMessage || currMessage.role !== 'user') return
|
||||
this._setCurrentMessageState(newState, messageIdx)
|
||||
}
|
||||
|
||||
_useCurrentThreadState() {
|
||||
const thread = this.getCurrentThread()
|
||||
|
||||
const state = thread.state
|
||||
const setState = this._setCurrentThreadState.bind(this)
|
||||
|
||||
return [state, setState] as const
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -3,6 +3,7 @@
|
|||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { OnText } from '../../common/llmMessageTypes.js'
|
||||
import { DIVIDER, FINAL, ORIGINAL } from '../prompt/prompts.js'
|
||||
|
||||
class SurroundingsRemover {
|
||||
|
|
@ -59,7 +60,7 @@ class SurroundingsRemover {
|
|||
// return offset === suffix.length
|
||||
// }
|
||||
|
||||
removeFromStartUntil = (until: string, alsoRemoveUntilStr: boolean) => {
|
||||
removeFromStartUntilFullMatch = (until: string, alsoRemoveUntilStr: boolean) => {
|
||||
const index = this.originalS.indexOf(until, this.i)
|
||||
|
||||
if (index === -1) {
|
||||
|
|
@ -86,7 +87,7 @@ class SurroundingsRemover {
|
|||
const foundCodeBlock = pm.removePrefix('```')
|
||||
if (!foundCodeBlock) return false
|
||||
|
||||
pm.removeFromStartUntil('\n', true) // language
|
||||
pm.removeFromStartUntilFullMatch('\n', true) // language
|
||||
|
||||
const j = pm.j
|
||||
let foundCodeBlockEnd = pm.removeSuffix('```')
|
||||
|
|
@ -159,27 +160,10 @@ export const extractCodeFromFIM = ({ text, recentlyAddedTextLen, midTag, }: { te
|
|||
const [delta, ignoredSuffix] = pm.deltaInfo(recentlyAddedTextLen)
|
||||
|
||||
return [s, delta, ignoredSuffix]
|
||||
|
||||
|
||||
// // const regex = /[\s\S]*?(?:`{1,3}\s*([a-zA-Z_]+[\w]*)?[\s\S]*?)?<MID>([\s\S]*?)(?:<\/MID>|`{1,3}|$)/;
|
||||
// const regex = new RegExp(
|
||||
// `[\\s\\S]*?(?:\`{1,3}\\s*([a-zA-Z_]+[\\w]*)?[\\s\\S]*?)?<${midTag}>([\\s\\S]*?)(?:</${midTag}>|\`{1,3}|$)`,
|
||||
// ''
|
||||
// );
|
||||
// const match = text.match(regex);
|
||||
// if (match) {
|
||||
// const [_, languageName, codeBetweenMidTags] = match;
|
||||
// return [languageName, codeBetweenMidTags] as const
|
||||
|
||||
// } else {
|
||||
// return [undefined, extractCodeFromRegular(text)] as const
|
||||
// }
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
export type ExtractedSearchReplaceBlock = {
|
||||
state: 'writingOriginal' | 'writingFinal' | 'done',
|
||||
orig: string,
|
||||
|
|
@ -201,7 +185,7 @@ export const extractSearchReplaceBlocks = (str: string) => {
|
|||
|
||||
const ORIGINAL_ = ORIGINAL + `\n`
|
||||
const DIVIDER_ = '\n' + DIVIDER + `\n`
|
||||
const FINAL_ = '\n' + FINAL
|
||||
// logic for FINAL_ is slightly more complicated - should be '\n' + FINAL, but that ignores if the final output is empty
|
||||
|
||||
|
||||
const blocks: ExtractedSearchReplaceBlock[] = []
|
||||
|
|
@ -229,7 +213,13 @@ export const extractSearchReplaceBlocks = (str: string) => {
|
|||
i = dividerStart
|
||||
// wrote =====
|
||||
|
||||
let finalStart = str.indexOf(FINAL_, i)
|
||||
|
||||
|
||||
const finalStartA = str.indexOf(FINAL, i)
|
||||
const finalStartB = str.indexOf('\n' + FINAL, i) // go with B if possible, else fallback to A, it's more permissive
|
||||
const FINAL_ = finalStartB !== -1 ? '\n' + FINAL : FINAL
|
||||
let finalStart = finalStartB !== -1 ? finalStartB : finalStartA
|
||||
|
||||
if (finalStart === -1) { // if didnt find FINAL_, either writing finalStr or FINAL_ right now
|
||||
const isWritingFINAL = endsWithAnyPrefixOf(str, FINAL_)
|
||||
blocks.push({
|
||||
|
|
@ -251,3 +241,96 @@ export const extractSearchReplaceBlocks = (str: string) => {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// could simplify this - this assumes we can never add a tag without committing it to the user's screen, but that's not true
|
||||
export const extractReasoningFromText = (
|
||||
onText_: OnText,
|
||||
thinkTags: [string, string],
|
||||
): OnText => {
|
||||
|
||||
let latestAddIdx = 0 // exclusive
|
||||
let foundTag1 = false
|
||||
let foundTag2 = false
|
||||
|
||||
let fullText = ''
|
||||
let fullReasoning = ''
|
||||
|
||||
const onText: OnText = ({ newText: newText_, fullText: fullText_ }) => {
|
||||
// abcdef<t|hin|k>ghi
|
||||
// |
|
||||
// until found the first think tag, keep adding to fullText
|
||||
if (!foundTag1) {
|
||||
const endsWithTag1 = endsWithAnyPrefixOf(fullText_, thinkTags[0])
|
||||
if (endsWithTag1) {
|
||||
// wait until we get the full tag or know more
|
||||
return
|
||||
}
|
||||
// if found the first tag
|
||||
const tag1Index = fullText_.lastIndexOf(thinkTags[0])
|
||||
if (tag1Index !== -1) {
|
||||
foundTag1 = true
|
||||
const newText = fullText.substring(latestAddIdx, tag1Index)
|
||||
const newReasoning = fullText.substring(tag1Index + thinkTags[0].length, Infinity)
|
||||
|
||||
fullText += newText
|
||||
fullReasoning += newReasoning
|
||||
latestAddIdx += newText.length + newReasoning.length
|
||||
onText_({ newText, fullText, newReasoning: newReasoning, fullReasoning })
|
||||
return
|
||||
}
|
||||
|
||||
// add the text to fullText
|
||||
const newText = fullText.substring(latestAddIdx, Infinity)
|
||||
fullText += newText
|
||||
latestAddIdx += newText.length
|
||||
onText_({ newText, fullText, newReasoning: '', fullReasoning })
|
||||
return
|
||||
}
|
||||
// at this point, we found <tag1>
|
||||
|
||||
// until found the second think tag, keep adding to fullReasoning
|
||||
if (!foundTag2) {
|
||||
const endsWithTag2 = endsWithAnyPrefixOf(fullText_, thinkTags[1])
|
||||
if (endsWithTag2) {
|
||||
// wait until we get the full tag or know more
|
||||
return
|
||||
}
|
||||
// if found the second tag
|
||||
const tag2Index = fullText_.lastIndexOf(thinkTags[1])
|
||||
if (tag2Index !== -1) {
|
||||
foundTag2 = true
|
||||
const newReasoning = fullText.substring(latestAddIdx, tag2Index)
|
||||
const newText = fullText.substring(tag2Index + thinkTags[1].length, Infinity)
|
||||
|
||||
fullText += newText
|
||||
fullReasoning += newReasoning
|
||||
latestAddIdx += newText.length + newReasoning.length
|
||||
onText_({ newText, fullText, newReasoning: newReasoning, fullReasoning })
|
||||
return
|
||||
}
|
||||
|
||||
// add the text to fullReasoning
|
||||
const newReasoning = fullText.substring(latestAddIdx, Infinity)
|
||||
fullReasoning += newReasoning
|
||||
latestAddIdx += newReasoning.length
|
||||
onText_({ newText: '', fullText, newReasoning, fullReasoning })
|
||||
return
|
||||
}
|
||||
// at this point, we found <tag2>
|
||||
|
||||
fullText += newText_
|
||||
const newText = fullText.substring(latestAddIdx, Infinity)
|
||||
latestAddIdx += newText.length
|
||||
onText_({ newText, fullText, newReasoning: '', fullReasoning })
|
||||
}
|
||||
|
||||
return onText
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,10 +7,9 @@
|
|||
import { URI } from '../../../../../base/common/uri.js';
|
||||
import { filenameToVscodeLanguage } from '../helpers/detectLanguage.js';
|
||||
import { CodeSelection, StagingSelectionItem, FileSelection } from '../chatThreadService.js';
|
||||
import { VSReadFile } from '../helpers/readFile.js';
|
||||
import { IModelService } from '../../../../../editor/common/services/model.js';
|
||||
import { os } from '../helpers/systemInfo.js';
|
||||
import { IFileService } from '../../../../../platform/files/common/files.js';
|
||||
import { IVoidFileService } from '../../common/voidFileService.js';
|
||||
|
||||
|
||||
// this is just for ease of readability
|
||||
|
|
@ -169,10 +168,10 @@ ${tripleTick[1]}
|
|||
}
|
||||
|
||||
const failToReadStr = 'Could not read content. This file may have been deleted. If you expected content here, you can tell the user about this as they might not know.'
|
||||
const stringifyFileSelections = async (fileSelections: FileSelection[], modelService: IModelService, fileService: IFileService) => {
|
||||
const stringifyFileSelections = async (fileSelections: FileSelection[], voidFileService: IVoidFileService) => {
|
||||
if (fileSelections.length === 0) return null
|
||||
const fileSlns: FileSelnLocal[] = await Promise.all(fileSelections.map(async (sel) => {
|
||||
const content = await VSReadFile(sel.fileURI, modelService, fileService) ?? failToReadStr
|
||||
const content = await voidFileService.readFile(sel.fileURI) ?? failToReadStr
|
||||
return { ...sel, content }
|
||||
}))
|
||||
return fileSlns.map(sel => stringifyFileSelection(sel)).join('\n')
|
||||
|
|
@ -195,7 +194,7 @@ export const chat_userMessageContent = async (instructions: string, currSelns: S
|
|||
return str;
|
||||
};
|
||||
|
||||
export const chat_selectionsString = async (prevSelns: StagingSelectionItem[] | null, currSelns: StagingSelectionItem[] | null, modelService: IModelService, fileService: IFileService) => {
|
||||
export const chat_selectionsString = async (prevSelns: StagingSelectionItem[] | null, currSelns: StagingSelectionItem[] | null, voidFileService: IVoidFileService) => {
|
||||
|
||||
// ADD IN FILES AT TOP
|
||||
const allSelections = [...currSelns || [], ...prevSelns || []]
|
||||
|
|
@ -220,7 +219,7 @@ export const chat_selectionsString = async (prevSelns: StagingSelectionItem[] |
|
|||
}
|
||||
}
|
||||
|
||||
const filesStr = await stringifyFileSelections(fileSelections, modelService, fileService)
|
||||
const filesStr = await stringifyFileSelections(fileSelections, voidFileService)
|
||||
const selnsStr = stringifyCodeSelections(codeSelections)
|
||||
|
||||
|
||||
|
|
@ -297,12 +296,12 @@ For example, if the user is asking you to "make this variable a better name", ma
|
|||
- Make sure you give enough context in the code block to apply the changes to the correct location in the code`
|
||||
|
||||
|
||||
export const aiRegex_computeReplacementsForFile_userMessage = async ({ searchClause, replaceClause, fileURI, modelService, fileService }: { searchClause: string, replaceClause: string, fileURI: URI, modelService: IModelService, fileService: IFileService }) => {
|
||||
export const aiRegex_computeReplacementsForFile_userMessage = async ({ searchClause, replaceClause, fileURI, voidFileService }: { searchClause: string, replaceClause: string, fileURI: URI, modelService: IModelService, voidFileService: IVoidFileService }) => {
|
||||
|
||||
// we may want to do this in batches
|
||||
const fileSelection: FileSelection = { type: 'File', fileURI, selectionStr: null, range: null }
|
||||
|
||||
const file = await stringifyFileSelections([fileSelection], modelService, fileService)
|
||||
const file = await stringifyFileSelections([fileSelection], voidFileService)
|
||||
|
||||
return `\
|
||||
## FILE
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ import { IMetricsService } from '../common/metricsService.js';
|
|||
|
||||
export type QuickEditPropsType = {
|
||||
diffareaid: number,
|
||||
initStreamingDiffZoneId: number | null,
|
||||
textAreaRef: (ref: HTMLTextAreaElement | null) => void;
|
||||
onChangeHeight: (height: number) => void;
|
||||
onChangeText: (text: string) => void;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,156 @@
|
|||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { useAccessor, useURIStreamState, useSettingsState } from '../util/services.js'
|
||||
import { useRefState } from '../util/helpers.js'
|
||||
import { isFeatureNameDisabled } from '../../../../common/voidSettingsTypes.js'
|
||||
import { URI } from '../../../../../../../base/common/uri.js'
|
||||
import { IEditCodeService, URIStreamState } from '../../../editCodeService.js'
|
||||
|
||||
enum CopyButtonText {
|
||||
Idle = 'Copy',
|
||||
Copied = 'Copied!',
|
||||
Error = 'Could not copy',
|
||||
}
|
||||
|
||||
const COPY_FEEDBACK_TIMEOUT = 1000 // amount of time to say 'Copied!'
|
||||
|
||||
const CopyButton = ({ codeStr }: { codeStr: string }) => {
|
||||
const accessor = useAccessor()
|
||||
|
||||
const metricsService = accessor.get('IMetricsService')
|
||||
const clipboardService = accessor.get('IClipboardService')
|
||||
const [copyButtonText, setCopyButtonText] = useState(CopyButtonText.Idle)
|
||||
|
||||
useEffect(() => {
|
||||
if (copyButtonText === CopyButtonText.Idle) return
|
||||
setTimeout(() => {
|
||||
setCopyButtonText(CopyButtonText.Idle)
|
||||
}, COPY_FEEDBACK_TIMEOUT)
|
||||
}, [copyButtonText])
|
||||
|
||||
|
||||
const onCopy = useCallback(() => {
|
||||
clipboardService.writeText(codeStr)
|
||||
.then(() => { setCopyButtonText(CopyButtonText.Copied) })
|
||||
.catch(() => { setCopyButtonText(CopyButtonText.Error) })
|
||||
metricsService.capture('Copy Code', { length: codeStr.length }) // capture the length only
|
||||
}, [metricsService, clipboardService, codeStr, setCopyButtonText])
|
||||
|
||||
const isSingleLine = !codeStr.includes('\n')
|
||||
|
||||
return <button
|
||||
className={`${isSingleLine ? '' : 'px-1 py-0.5'} text-sm bg-void-bg-1 text-void-fg-1 hover:brightness-110 border border-vscode-input-border rounded`}
|
||||
onClick={onCopy}
|
||||
>
|
||||
{copyButtonText}
|
||||
</button>
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// state persisted for duration of react only
|
||||
const applyingURIOfApplyBoxIdRef: { current: { [applyBoxId: string]: URI | undefined } } = { current: {} }
|
||||
|
||||
|
||||
|
||||
export const ApplyBlockHoverButtons = ({ codeStr, applyBoxId }: { codeStr: string, applyBoxId: string }) => {
|
||||
|
||||
console.log('applyboxid', applyBoxId, applyingURIOfApplyBoxIdRef)
|
||||
|
||||
const settingsState = useSettingsState()
|
||||
const isDisabled = !!isFeatureNameDisabled('Apply', settingsState) || !applyBoxId
|
||||
|
||||
const accessor = useAccessor()
|
||||
const editCodeService = accessor.get('IEditCodeService')
|
||||
const metricsService = accessor.get('IMetricsService')
|
||||
|
||||
const [_, rerender] = useState(0)
|
||||
|
||||
const applyingUri = useCallback(() => applyingURIOfApplyBoxIdRef.current[applyBoxId] ?? null, [applyBoxId])
|
||||
const streamState = useCallback(() => editCodeService.getURIStreamState({ uri: applyingUri() }), [editCodeService, applyingUri])
|
||||
|
||||
// listen for stream updates
|
||||
useURIStreamState(
|
||||
useCallback((uri, newStreamState) => {
|
||||
const shouldUpdate = applyingUri()?.fsPath !== uri.fsPath
|
||||
if (shouldUpdate) return
|
||||
rerender(c => c + 1)
|
||||
}, [applyBoxId, editCodeService, applyingUri])
|
||||
)
|
||||
|
||||
const onSubmit = useCallback(() => {
|
||||
if (isDisabled) return
|
||||
if (streamState() === 'streaming') return
|
||||
const newApplyingUri = editCodeService.startApplying({
|
||||
from: 'ClickApply',
|
||||
type: 'searchReplace',
|
||||
applyStr: codeStr,
|
||||
})
|
||||
applyingURIOfApplyBoxIdRef.current[applyBoxId] = newApplyingUri ?? undefined
|
||||
rerender(c => c + 1)
|
||||
metricsService.capture('Apply Code', { length: codeStr.length }) // capture the length only
|
||||
}, [isDisabled, streamState, editCodeService, codeStr, applyBoxId, metricsService])
|
||||
|
||||
|
||||
const onInterrupt = useCallback(() => {
|
||||
if (streamState() !== 'streaming') return
|
||||
const uri = applyingUri()
|
||||
if (!uri) return
|
||||
|
||||
editCodeService.interruptURIStreaming({ uri })
|
||||
metricsService.capture('Stop Apply', {})
|
||||
}, [streamState, applyingUri, editCodeService, metricsService])
|
||||
|
||||
|
||||
const isSingleLine = !codeStr.includes('\n')
|
||||
|
||||
const applyButton = <button
|
||||
// btn btn-secondary btn-sm border text-sm border-vscode-input-border rounded
|
||||
className={`${isSingleLine ? '' : 'px-1 py-0.5'} text-sm bg-void-bg-1 text-void-fg-1 hover:brightness-110 border border-vscode-input-border rounded`}
|
||||
onClick={onSubmit}
|
||||
>
|
||||
Apply
|
||||
</button>
|
||||
|
||||
const stopButton = <button
|
||||
// btn btn-secondary btn-sm border text-sm border-vscode-input-border rounded
|
||||
className={`${isSingleLine ? '' : 'px-1 py-0.5'} text-sm bg-void-bg-1 text-void-fg-1 hover:brightness-110 border border-vscode-input-border rounded`}
|
||||
onClick={onInterrupt}
|
||||
>
|
||||
Stop
|
||||
</button>
|
||||
|
||||
const acceptRejectButtons = <>
|
||||
<button
|
||||
// btn btn-secondary btn-sm border text-sm border-vscode-input-border rounded
|
||||
className={`${isSingleLine ? '' : 'px-1 py-0.5'} text-sm bg-void-bg-1 text-void-fg-1 hover:brightness-110 border border-vscode-input-border rounded`}
|
||||
onClick={() => {
|
||||
const uri = applyingUri()
|
||||
if (uri) editCodeService.removeDiffAreas({ uri, behavior: 'accept', removeCtrlKs: false })
|
||||
}}
|
||||
>
|
||||
Accept
|
||||
</button>
|
||||
<button
|
||||
// btn btn-secondary btn-sm border text-sm border-vscode-input-border rounded
|
||||
className={`${isSingleLine ? '' : 'px-1 py-0.5'} text-sm bg-void-bg-1 text-void-fg-1 hover:brightness-110 border border-vscode-input-border rounded`}
|
||||
onClick={() => {
|
||||
const uri = applyingUri()
|
||||
if (uri) editCodeService.removeDiffAreas({ uri, behavior: 'reject', removeCtrlKs: false })
|
||||
}}
|
||||
>
|
||||
Reject
|
||||
</button>
|
||||
</>
|
||||
|
||||
console.log('streamStateRef.current', streamState())
|
||||
|
||||
const currStreamState = streamState()
|
||||
return <>
|
||||
{currStreamState !== 'streaming' && <CopyButton codeStr={codeStr} />}
|
||||
{currStreamState === 'idle' && !isDisabled && applyButton}
|
||||
{currStreamState === 'streaming' && stopButton}
|
||||
{currStreamState === 'acceptRejectAll' && acceptRejectButtons}
|
||||
</>
|
||||
}
|
||||
|
|
@ -3,22 +3,12 @@
|
|||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import React, { JSX, useCallback, useEffect, useState } from 'react'
|
||||
import React, { JSX } from 'react'
|
||||
import { marked, MarkedToken, Token } from 'marked'
|
||||
import { BlockCode } from './BlockCode.js'
|
||||
import { useAccessor, useChatThreadsState, useChatThreadsStreamState } from '../util/services.js'
|
||||
import { ChatMessageLocation, } from '../../../aiRegexService.js'
|
||||
import { nameToVscodeLanguage } from '../../../helpers/detectLanguage.js'
|
||||
|
||||
|
||||
enum CopyButtonState {
|
||||
Copy = 'Copy',
|
||||
Copied = 'Copied!',
|
||||
Error = 'Could not copy',
|
||||
}
|
||||
|
||||
const COPY_FEEDBACK_TIMEOUT = 1000 // amount of time to say 'Copied!'
|
||||
|
||||
import { ApplyBlockHoverButtons } from './ApplyBlockHoverButtons.js'
|
||||
|
||||
|
||||
type ApplyBoxLocation = ChatMessageLocation & { tokenIdx: string }
|
||||
|
|
@ -29,60 +19,6 @@ const getApplyBoxId = ({ threadId, messageIdx, tokenIdx }: ApplyBoxLocation) =>
|
|||
|
||||
|
||||
|
||||
const ApplyButtonsOnHover = ({ applyStr }: { applyStr: string }) => {
|
||||
const accessor = useAccessor()
|
||||
|
||||
const [copyButtonState, setCopyButtonState] = useState(CopyButtonState.Copy)
|
||||
const editCodeService = accessor.get('IEditCodeService')
|
||||
const clipboardService = accessor.get('IClipboardService')
|
||||
const metricsService = accessor.get('IMetricsService')
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
if (copyButtonState !== CopyButtonState.Copy) {
|
||||
setTimeout(() => {
|
||||
setCopyButtonState(CopyButtonState.Copy)
|
||||
}, COPY_FEEDBACK_TIMEOUT)
|
||||
}
|
||||
}, [copyButtonState])
|
||||
|
||||
const onCopy = useCallback(() => {
|
||||
clipboardService.writeText(applyStr)
|
||||
.then(() => { setCopyButtonState(CopyButtonState.Copied) })
|
||||
.catch(() => { setCopyButtonState(CopyButtonState.Error) })
|
||||
metricsService.capture('Copy Code', { length: applyStr.length }) // capture the length only
|
||||
|
||||
}, [metricsService, clipboardService, applyStr])
|
||||
|
||||
const onApply = useCallback(() => {
|
||||
|
||||
editCodeService.startApplying({
|
||||
from: 'ClickApply',
|
||||
type: 'searchReplace',
|
||||
applyStr,
|
||||
})
|
||||
metricsService.capture('Apply Code', { length: applyStr.length }) // capture the length only
|
||||
}, [metricsService, editCodeService, applyStr])
|
||||
|
||||
const isSingleLine = !applyStr.includes('\n')
|
||||
|
||||
return <>
|
||||
<button
|
||||
className={`${isSingleLine ? '' : 'px-1 py-0.5'} text-sm bg-void-bg-1 text-void-fg-1 hover:brightness-110 border border-vscode-input-border rounded`}
|
||||
onClick={onCopy}
|
||||
>
|
||||
{copyButtonState}
|
||||
</button>
|
||||
<button
|
||||
// btn btn-secondary btn-sm border text-sm border-vscode-input-border rounded
|
||||
className={`${isSingleLine ? '' : 'px-1 py-0.5'} text-sm bg-void-bg-1 text-void-fg-1 hover:brightness-110 border border-vscode-input-border rounded`}
|
||||
onClick={onApply}
|
||||
>
|
||||
Apply
|
||||
</button>
|
||||
</>
|
||||
}
|
||||
|
||||
export const CodeSpan = ({ children, className }: { children: React.ReactNode, className?: string }) => {
|
||||
return <code className={`
|
||||
bg-void-bg-1
|
||||
|
|
@ -97,7 +33,7 @@ export const CodeSpan = ({ children, className }: { children: React.ReactNode, c
|
|||
</code>
|
||||
}
|
||||
|
||||
const RenderToken = ({ token, nested = false, noSpace = false, chatMessageLocation, tokenIdx }: { token: Token | string, nested?: boolean, noSpace?: boolean, chatMessageLocation?: ChatMessageLocation, tokenIdx: string }): JSX.Element => {
|
||||
const RenderToken = ({ token, nested, noSpace, chatMessageLocation, tokenIdx }: { token: Token | string, nested?: boolean, noSpace?: boolean, chatMessageLocation?: ChatMessageLocation, tokenIdx: string }): JSX.Element => {
|
||||
|
||||
|
||||
// deal with built-in tokens first (assume marked token)
|
||||
|
|
@ -108,9 +44,7 @@ const RenderToken = ({ token, nested = false, noSpace = false, chatMessageLocati
|
|||
}
|
||||
|
||||
if (t.type === "code") {
|
||||
const isCodeblockClosed = t.raw?.startsWith('```') && t.raw?.endsWith('```');
|
||||
|
||||
// this should never be
|
||||
const applyBoxId = chatMessageLocation ? getApplyBoxId({
|
||||
threadId: chatMessageLocation.threadId,
|
||||
messageIdx: chatMessageLocation.messageIdx,
|
||||
|
|
@ -120,7 +54,7 @@ const RenderToken = ({ token, nested = false, noSpace = false, chatMessageLocati
|
|||
return <BlockCode
|
||||
initValue={t.text}
|
||||
language={t.lang === undefined ? undefined : nameToVscodeLanguage[t.lang]}
|
||||
buttonsOnHover={applyBoxId && <ApplyButtonsOnHover applyStr={t.text} />}
|
||||
buttonsOnHover={applyBoxId && <ApplyBlockHoverButtons applyBoxId={applyBoxId} codeStr={t.text} />}
|
||||
/>
|
||||
}
|
||||
|
||||
|
|
@ -201,6 +135,24 @@ const RenderToken = ({ token, nested = false, noSpace = false, chatMessageLocati
|
|||
))}
|
||||
</ListTag>
|
||||
)
|
||||
// attempt at indentation
|
||||
// return (
|
||||
// <ListTag
|
||||
// start={t.start ? t.start : undefined}
|
||||
// className={`pl-2 ${noSpace ? '' : 'my-4'} ${t.ordered ? "list-decimal" : "list-disc"}`}
|
||||
// >
|
||||
// {t.items.map((item, index) => (
|
||||
// <li key={index} className={`${noSpace ? '' : 'mb-2'} ml-4`}>
|
||||
// {item.task && (
|
||||
// <input type="checkbox" className='mr-2 form-checkbox' checked={item.checked} readOnly />
|
||||
// )}
|
||||
// <span className-='inline-block pr-2'>
|
||||
// <ChatMarkdownRender chatMessageLocation={chatMessageLocation} string={item.text} nested={true} />
|
||||
// </span>
|
||||
// </li>
|
||||
// ))}
|
||||
// </ListTag>
|
||||
// )
|
||||
}
|
||||
|
||||
if (t.type === "paragraph") {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import React, { FormEvent, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useSettingsState, useSidebarState, useChatThreadsState, useQuickEditState, useAccessor } from '../util/services.js';
|
||||
import { useSettingsState, useSidebarState, useChatThreadsState, useQuickEditState, useAccessor, useCtrlKZoneStreamingState } from '../util/services.js';
|
||||
import { TextAreaFns, VoidInputBox2 } from '../util/inputs.js';
|
||||
import { QuickEditPropsType } from '../../../quickEditActions.js';
|
||||
import { ButtonStop, ButtonSubmit, IconX, VoidChatArea } from '../sidebar-tsx/SidebarChat.js';
|
||||
|
|
@ -16,7 +16,6 @@ import { isFeatureNameDisabled } from '../../../../../../../workbench/contrib/vo
|
|||
|
||||
export const QuickEditChat = ({
|
||||
diffareaid,
|
||||
initStreamingDiffZoneId,
|
||||
onChangeHeight,
|
||||
onChangeText: onChangeText_,
|
||||
textAreaRef: textAreaRef_,
|
||||
|
|
@ -49,28 +48,31 @@ export const QuickEditChat = ({
|
|||
const [instructionsAreEmpty, setInstructionsAreEmpty] = useState(!(initText ?? '')) // the user's instructions
|
||||
const isDisabled = instructionsAreEmpty || !!isFeatureNameDisabled('Ctrl+K', settingsState)
|
||||
|
||||
const [currStreamingDiffZoneRef, setCurrentlyStreamingDiffZone] = useRefState<number | null>(initStreamingDiffZoneId)
|
||||
const isStreaming = currStreamingDiffZoneRef.current !== null
|
||||
|
||||
const [isStreamingRef, setIsStreamingRef] = useRefState(editCodeService.isCtrlKZoneStreaming({ diffareaid }))
|
||||
useCtrlKZoneStreamingState(useCallback((diffareaid2, isStreaming) => {
|
||||
if (diffareaid !== diffareaid2) return
|
||||
setIsStreamingRef(isStreaming)
|
||||
}, [diffareaid, setIsStreamingRef]))
|
||||
|
||||
|
||||
const onSubmit = useCallback(() => {
|
||||
if (isDisabled) return
|
||||
if (currStreamingDiffZoneRef.current !== null) return
|
||||
if (isStreamingRef.current) return
|
||||
textAreaFnsRef.current?.disable()
|
||||
|
||||
const id = editCodeService.startApplying({
|
||||
editCodeService.startApplying({
|
||||
from: 'QuickEdit',
|
||||
type:'rewrite',
|
||||
diffareaid: diffareaid,
|
||||
type: 'rewrite',
|
||||
diffareaid,
|
||||
})
|
||||
setCurrentlyStreamingDiffZone(id ?? null)
|
||||
}, [currStreamingDiffZoneRef, setCurrentlyStreamingDiffZone, isDisabled, editCodeService, diffareaid])
|
||||
}, [isStreamingRef, isDisabled, editCodeService, diffareaid])
|
||||
|
||||
const onInterrupt = useCallback(() => {
|
||||
if (currStreamingDiffZoneRef.current === null) return
|
||||
editCodeService.interruptStreaming(currStreamingDiffZoneRef.current)
|
||||
setCurrentlyStreamingDiffZone(null)
|
||||
if (!isStreamingRef.current) return
|
||||
editCodeService.interruptCtrlKStreaming({ diffareaid })
|
||||
textAreaFnsRef.current?.enable()
|
||||
}, [currStreamingDiffZoneRef, setCurrentlyStreamingDiffZone, editCodeService])
|
||||
}, [isStreamingRef, editCodeService])
|
||||
|
||||
|
||||
const onX = useCallback(() => {
|
||||
|
|
@ -89,7 +91,7 @@ export const QuickEditChat = ({
|
|||
onSubmit={onSubmit}
|
||||
onAbort={onInterrupt}
|
||||
onClose={onX}
|
||||
isStreaming={isStreaming}
|
||||
isStreaming={isStreamingRef.current}
|
||||
isDisabled={isDisabled}
|
||||
featureName="Ctrl+K"
|
||||
className="py-2 w-full"
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import React, { ButtonHTMLAttributes, FormEvent, FormHTMLAttributes, Fragment, K
|
|||
|
||||
|
||||
import { useAccessor, useSidebarState, useChatThreadsState, useChatThreadsStreamState, useUriState, useSettingsState } from '../util/services.js';
|
||||
import { ChatMessage, StagingSelectionItem } from '../../../chatThreadService.js';
|
||||
import { ChatMessage, StagingSelectionItem, ToolMessage } from '../../../chatThreadService.js';
|
||||
|
||||
import { BlockCode } from '../markdown/BlockCode.js';
|
||||
import { ChatMarkdownRender } from '../markdown/ChatMarkdownRender.js';
|
||||
|
|
@ -21,10 +21,11 @@ import { useScrollbarStyles } from '../util/useScrollbarStyles.js';
|
|||
import { VOID_CTRL_L_ACTION_ID } from '../../../actionIDs.js';
|
||||
import { filenameToVscodeLanguage } from '../../../helpers/detectLanguage.js';
|
||||
import { VOID_OPEN_SETTINGS_ACTION_ID } from '../../../voidSettingsPane.js';
|
||||
import { Pencil, X } from 'lucide-react';
|
||||
import { ChevronRight, Pencil, X } from 'lucide-react';
|
||||
import { FeatureName, isFeatureNameDisabled } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js';
|
||||
import { WarningBox } from '../void-settings-tsx/WarningBox.js';
|
||||
import { ChatMessageLocation } from '../../../aiRegexService.js';
|
||||
import { ToolCallReturnType, ToolName } from '../../../../common/toolsService.js';
|
||||
|
||||
|
||||
|
||||
|
|
@ -542,6 +543,146 @@ export const SelectedFiles = (
|
|||
}
|
||||
|
||||
|
||||
type ToolReusltToComponent = { [T in ToolName]: (props: { message: ToolMessage<T> }) => React.ReactNode }
|
||||
interface ToolResultProps {
|
||||
actionTitle: string;
|
||||
actionParam: string;
|
||||
actionNumResults?: number;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
const ToolResult = ({
|
||||
actionTitle,
|
||||
actionParam,
|
||||
actionNumResults,
|
||||
children,
|
||||
}: ToolResultProps) => {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
const isDropdown = !!children
|
||||
|
||||
return (
|
||||
<div className="mx-4 select-none">
|
||||
<div className="border border-void-border-3 rounded px-1 py-0.5 bg-void-bg-tool">
|
||||
<div
|
||||
className={`flex items-center min-h-[24px] ${isDropdown ? 'cursor-pointer hover:brightness-125 transition-all duration-150' : 'mx-1'}`}
|
||||
onClick={() => children && setIsExpanded(!isExpanded)}
|
||||
>
|
||||
{isDropdown && (
|
||||
<ChevronRight
|
||||
className={`text-void-fg-3 mr-0.5 h-5 w-5 flex-shrink-0 transition-transform duration-100 ease-[cubic-bezier(0.4,0,0.2,1)] ${isExpanded ? 'rotate-90' : ''}`}
|
||||
/>
|
||||
)}
|
||||
<div className="flex items-center flex-wrap gap-x-2 gap-y-0.5">
|
||||
<span className="text-void-fg-3">{actionTitle}</span>
|
||||
<span className="text-void-fg-4 text-xs italic">{`"`}{actionParam}{`"`}</span>
|
||||
{actionNumResults !== undefined && (
|
||||
<span className="text-void-fg-4 text-xs">
|
||||
{`(`}{actionNumResults}{` result`}{actionNumResults !== 1 ? 's' : ''}{`)`}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={`mt-1 overflow-hidden transition-all duration-200 ease-in-out ${isExpanded ? 'max-h-[500px] opacity-100' : 'max-h-0 opacity-0'}`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
|
||||
const toolResultToComponent: ToolReusltToComponent = {
|
||||
'read_file': ({ message }) => (
|
||||
<ToolResult
|
||||
actionTitle="Read file"
|
||||
actionParam={getBasename(message.result.uri.fsPath)}
|
||||
/>
|
||||
),
|
||||
'list_dir': ({ message }) => (
|
||||
<ToolResult
|
||||
actionTitle="Inspected folder"
|
||||
actionParam={`${getBasename(message.result.rootURI.fsPath)}/`}
|
||||
actionNumResults={message.result.children?.length}
|
||||
>
|
||||
<div className="text-void-fg-2">
|
||||
{message.result.children?.map((item, i) => (
|
||||
<div key={i} className="pl-2 py-0.5 mb-1 bg-void-bg-1 rounded">
|
||||
{item.name}
|
||||
{item.isDirectory && '/'}
|
||||
</div>
|
||||
))}
|
||||
{message.result.hasNextPage && (
|
||||
<div className="pl-2 text-void-fg-3 italic">
|
||||
{message.result.itemsRemaining} more items...
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ToolResult>
|
||||
),
|
||||
'pathname_search': ({ message }) => (
|
||||
<ToolResult
|
||||
actionTitle="Searched filename"
|
||||
actionParam={message.result.queryStr}
|
||||
actionNumResults={Array.isArray(message.result.uris) ? message.result.uris.length : 0}
|
||||
>
|
||||
<div className="text-void-fg-2">
|
||||
{Array.isArray(message.result.uris) ?
|
||||
message.result.uris.map((uri, i) => (
|
||||
<div key={i} className="pl-2 py-0.5 mb-1 bg-void-bg-1 rounded">
|
||||
<a
|
||||
href={uri.toString()}
|
||||
className="text-void-accent hover:underline"
|
||||
>
|
||||
{uri.fsPath.split('/').pop()}
|
||||
</a>
|
||||
</div>
|
||||
)) :
|
||||
<div className="pl-2">{message.result.uris}</div>
|
||||
}
|
||||
{message.result.hasNextPage && (
|
||||
<div className="pl-2 text-void-fg-3 italic">
|
||||
More results available...
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ToolResult>
|
||||
),
|
||||
'search': ({ message }) => (
|
||||
<ToolResult
|
||||
actionTitle="Searched"
|
||||
actionParam={message.result.queryStr}
|
||||
actionNumResults={Array.isArray(message.result.uris) ? message.result.uris.length : 0}
|
||||
>
|
||||
<div className="text-void-fg-2">
|
||||
{typeof message.result.uris === 'string' ?
|
||||
message.result.uris :
|
||||
message.result.uris.map((uri, i) => (
|
||||
<div key={i} className="pl-2 py-0.5 mb-1 bg-void-bg-1 rounded">
|
||||
<a
|
||||
href={uri.toString()}
|
||||
className="text-void-accent hover:underline"
|
||||
>
|
||||
{uri.fsPath}
|
||||
</a>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
{message.result.hasNextPage && (
|
||||
<div className="pl-2 text-void-fg-3 italic">
|
||||
More results available...
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ToolResult>
|
||||
)
|
||||
};
|
||||
|
||||
|
||||
|
||||
type ChatBubbleMode = 'display' | 'edit'
|
||||
const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatMessage, messageIdx?: number, isLoading?: boolean, }) => {
|
||||
|
||||
|
|
@ -552,16 +693,16 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatM
|
|||
|
||||
// global state
|
||||
let isBeingEdited = false
|
||||
let setIsBeingEdited = (v: boolean) => { }
|
||||
let stagingSelections: StagingSelectionItem[] = []
|
||||
let setStagingSelections = (s: StagingSelectionItem[]) => { }
|
||||
let setIsBeingEdited = (_: boolean) => { }
|
||||
let setStagingSelections = (_: StagingSelectionItem[]) => { }
|
||||
|
||||
if (messageIdx !== undefined) {
|
||||
const [_state, _setState] = chatThreadsService._useCurrentMessageState(messageIdx)
|
||||
const _state = chatThreadsService.getCurrentMessageState(messageIdx)
|
||||
isBeingEdited = _state.isBeingEdited
|
||||
setIsBeingEdited = (v) => _setState({ isBeingEdited: v })
|
||||
stagingSelections = _state.stagingSelections
|
||||
setStagingSelections = (s) => { _setState({ stagingSelections: s }) }
|
||||
setIsBeingEdited = (v) => chatThreadsService.setCurrentMessageState(messageIdx, { isBeingEdited: v })
|
||||
setStagingSelections = (s) => chatThreadsService.setCurrentMessageState(messageIdx, { stagingSelections: s })
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -590,7 +731,7 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatM
|
|||
_mustInitialize.current = false
|
||||
}
|
||||
|
||||
}, [role, mode, _justEnabledEdit, textAreaRefState, textAreaFnsRef.current, _justEnabledEdit.current, _mustInitialize.current])
|
||||
}, [chatMessage, role, mode, _justEnabledEdit, textAreaRefState, textAreaFnsRef.current, _justEnabledEdit.current, _mustInitialize.current])
|
||||
const EditSymbol = mode === 'display' ? Pencil : X
|
||||
const onOpenEdit = () => {
|
||||
setIsBeingEdited(true)
|
||||
|
|
@ -631,7 +772,7 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatM
|
|||
|
||||
// stream the edit
|
||||
const userMessage = textAreaRefState.value;
|
||||
await chatThreadsService.editUserMessageAndStreamResponse({ userMessage, chatMode: 'agent', messageIdx })
|
||||
await chatThreadsService.editUserMessageAndStreamResponse({ userMessage, chatMode: 'agent', messageIdx, })
|
||||
}
|
||||
|
||||
const onAbort = () => {
|
||||
|
|
@ -695,7 +836,13 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatM
|
|||
chatbubbleContents = <ChatMarkdownRender string={chatMessage.displayContent ?? ''} chatMessageLocation={chatMessageLocation} />
|
||||
}
|
||||
else if (role === 'tool') {
|
||||
chatbubbleContents = chatMessage.name
|
||||
|
||||
const ToolComponent = toolResultToComponent[chatMessage.name] as ({ message }: { message: any }) => React.ReactNode // ts isnt smart enough to deal with the types here...
|
||||
|
||||
chatbubbleContents = <ToolComponent message={chatMessage} />
|
||||
|
||||
console.log('tool result:', chatMessage.name, chatMessage.params, chatMessage.result)
|
||||
|
||||
}
|
||||
|
||||
return <div
|
||||
|
|
@ -703,7 +850,7 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatM
|
|||
className={`
|
||||
relative
|
||||
${mode === 'edit' ? 'px-2 w-full max-w-full'
|
||||
: role === 'user' ? `px-2 self-end w-fit max-w-full whitespace-pre-wrap` // user words should be pre
|
||||
: role === 'user' ? `my-0.5 px-2 self-end w-fit max-w-full whitespace-pre-wrap` // user words should be pre
|
||||
: role === 'assistant' ? `px-2 self-start w-full max-w-full` : ''
|
||||
}
|
||||
`}
|
||||
|
|
@ -780,9 +927,8 @@ export const SidebarChat = () => {
|
|||
const currentThread = chatThreadsService.getCurrentThread()
|
||||
const previousMessages = currentThread?.messages ?? []
|
||||
|
||||
const [_state, _setState] = chatThreadsService._useCurrentThreadState()
|
||||
const selections = _state.stagingSelections
|
||||
const setSelections = (s: StagingSelectionItem[]) => { _setState({ stagingSelections: s }) }
|
||||
const selections = chatThreadsService.getCurrentThread().state.stagingSelections
|
||||
const setSelections = (s: StagingSelectionItem[]) => { chatThreadsService.setCurrentThreadStagingSelections(s) }
|
||||
|
||||
// stream state
|
||||
const currThreadStreamState = useChatThreadsStreamState(chatThreadsState.currentThreadId)
|
||||
|
|
@ -818,7 +964,7 @@ export const SidebarChat = () => {
|
|||
textAreaFnsRef.current?.setValue('')
|
||||
textAreaRef.current?.focus() // focus input after submit
|
||||
|
||||
}, [chatThreadsService, isDisabled, isStreaming, textAreaRef, textAreaFnsRef, selections, setSelections])
|
||||
}, [chatThreadsService, isDisabled, isStreaming, textAreaRef, textAreaFnsRef, setSelections])
|
||||
|
||||
const onAbort = () => {
|
||||
const threadId = currentThread.id
|
||||
|
|
@ -874,7 +1020,7 @@ export const SidebarChat = () => {
|
|||
|
||||
{/* error message */}
|
||||
{latestError === undefined ? null :
|
||||
<div className='px-2'>
|
||||
<div className='px-2 my-1'>
|
||||
<ErrorDisplay
|
||||
message={latestError.message}
|
||||
fullError={latestError.fullError}
|
||||
|
|
|
|||
|
|
@ -68,13 +68,14 @@ export const SidebarThreadSelector = () => {
|
|||
let firstMsg = null;
|
||||
// let secondMsg = null;
|
||||
|
||||
const firstMsgIdx = pastThread.messages.findIndex(
|
||||
(msg) => msg.role !== 'system' && !!msg.displayContent
|
||||
const firstUserMsgIdx = pastThread.messages.findIndex(
|
||||
(msg) => msg.role !== 'system' && msg.role !== 'tool' && !!msg.displayContent
|
||||
);
|
||||
|
||||
if (firstMsgIdx !== -1) {
|
||||
if (firstUserMsgIdx !== -1) {
|
||||
// firstMsg = truncate(pastThread.messages[firstMsgIdx].displayContent ?? '');
|
||||
firstMsg = pastThread.messages[firstMsgIdx].displayContent ?? '';
|
||||
const firsUsertMsgObj = pastThread.messages[firstUserMsgIdx]
|
||||
firstMsg = firsUsertMsgObj.role === 'user' && firsUsertMsgObj.displayContent || '';
|
||||
} else {
|
||||
firstMsg = '""';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -152,12 +152,13 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
|
|||
|
||||
})
|
||||
|
||||
export const VoidInputBox = ({ onChangeText, onCreateInstance, inputBoxRef, placeholder, multiline }: {
|
||||
export const VoidInputBox = ({ onChangeText, onCreateInstance, inputBoxRef, placeholder, isPasswordField, multiline }: {
|
||||
onChangeText: (value: string) => void;
|
||||
styles?: Partial<IInputBoxStyles>,
|
||||
onCreateInstance?: (instance: InputBox) => void | IDisposable[];
|
||||
inputBoxRef?: { current: InputBox | null };
|
||||
placeholder: string;
|
||||
isPasswordField?: boolean;
|
||||
multiline: boolean;
|
||||
}) => {
|
||||
|
||||
|
|
@ -182,6 +183,7 @@ export const VoidInputBox = ({ onChangeText, onCreateInstance, inputBoxRef, plac
|
|||
},
|
||||
placeholder,
|
||||
tooltip: '',
|
||||
type: isPasswordField ? 'password' : undefined,
|
||||
flexibleHeight: multiline,
|
||||
flexibleMaxHeight: 500,
|
||||
flexibleWidth: false,
|
||||
|
|
@ -711,7 +713,7 @@ export const VoidCodeEditor = ({ initValue, language, maxHeight, showScrollbars
|
|||
|
||||
onCreateInstance={useCallback((editor: CodeEditorWidget) => {
|
||||
const model = modelOfEditorId[id] ?? modelService.createModel(
|
||||
initValueRef.current, {
|
||||
initValueRef.current + '\n', {
|
||||
languageId: languageRef.current ? languageRef.current : 'typescript',
|
||||
onDidChange: (e) => { return { dispose: () => { } } } // no idea why they'd require this
|
||||
})
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import React, { useState, useEffect, useCallback } from 'react'
|
||||
import { ThreadStreamState, ThreadsState } from '../../../chatThreadService.js'
|
||||
import { RefreshableProviderName, SettingsOfProvider } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js'
|
||||
import { IDisposable } from '../../../../../../../base/common/lifecycle.js'
|
||||
|
|
@ -14,10 +14,6 @@ import { VoidUriState } from '../../../voidUriStateService.js';
|
|||
import { VoidQuickEditState } from '../../../quickEditStateService.js'
|
||||
import { RefreshModelStateOfProvider } from '../../../../../../../workbench/contrib/void/common/refreshModelService.js'
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
import { ServicesAccessor } from '../../../../../../../editor/browser/editorExtensions.js';
|
||||
import { IModelService } from '../../../../../../../editor/common/services/model.js';
|
||||
import { IClipboardService } from '../../../../../../../platform/clipboard/common/clipboardService.js';
|
||||
|
|
@ -28,7 +24,7 @@ import { IThemeService } from '../../../../../../../platform/theme/common/themeS
|
|||
import { ILLMMessageService } from '../../../../../../../workbench/contrib/void/common/llmMessageService.js';
|
||||
import { IRefreshModelService } from '../../../../../../../workbench/contrib/void/common/refreshModelService.js';
|
||||
import { IVoidSettingsService } from '../../../../../../../workbench/contrib/void/common/voidSettingsService.js';
|
||||
import { IEditCodeService } from '../../../editCodeService.js';
|
||||
import { IEditCodeService, URIStreamState } from '../../../editCodeService.js';
|
||||
import { IVoidUriStateService } from '../../../voidUriStateService.js';
|
||||
import { IQuickEditStateService } from '../../../quickEditStateService.js';
|
||||
import { ISidebarStateService } from '../../../sidebarStateService.js';
|
||||
|
|
@ -47,6 +43,7 @@ import { IEnvironmentService } from '../../../../../../../platform/environment/c
|
|||
import { IConfigurationService } from '../../../../../../../platform/configuration/common/configuration.js'
|
||||
import { IPathService } from '../../../../../../../workbench/services/path/common/pathService.js'
|
||||
import { IMetricsService } from '../../../../../../../workbench/contrib/void/common/metricsService.js'
|
||||
import { URI } from '../../../../../../../base/common/uri.js'
|
||||
|
||||
|
||||
|
||||
|
|
@ -79,6 +76,11 @@ const refreshModelProviderListeners: Set<(p: RefreshableProviderName, s: Refresh
|
|||
let colorThemeState: ColorScheme
|
||||
const colorThemeStateListeners: Set<(s: ColorScheme) => void> = new Set()
|
||||
|
||||
const ctrlKZoneStreamingStateListeners: Set<(diffareaid: number, s: boolean) => void> = new Set()
|
||||
const uriStreamingStateListeners: Set<(uri: URI, s: URIStreamState) => void> = new Set()
|
||||
|
||||
|
||||
|
||||
// must call this before you can use any of the hooks below
|
||||
// this should only be called ONCE! this is the only place you don't need to dispose onDidChange. If you use state.onDidChange anywhere else, make sure to dispose it!
|
||||
let wasCalled = false
|
||||
|
|
@ -162,7 +164,7 @@ export const _registerServices = (accessor: ServicesAccessor) => {
|
|||
refreshModelService.onDidChangeState((providerName) => {
|
||||
refreshModelState = refreshModelService.state
|
||||
refreshModelStateListeners.forEach(l => l(refreshModelState))
|
||||
refreshModelProviderListeners.forEach(l => l(providerName, refreshModelState))
|
||||
refreshModelProviderListeners.forEach(l => l(providerName, refreshModelState)) // no state
|
||||
})
|
||||
)
|
||||
|
||||
|
|
@ -174,6 +176,21 @@ export const _registerServices = (accessor: ServicesAccessor) => {
|
|||
})
|
||||
)
|
||||
|
||||
// no state
|
||||
disposables.push(
|
||||
editCodeService.onDidChangeCtrlKZoneStreaming(({ diffareaid }) => {
|
||||
const isStreaming = editCodeService.isCtrlKZoneStreaming({ diffareaid })
|
||||
ctrlKZoneStreamingStateListeners.forEach(l => l(diffareaid, isStreaming))
|
||||
})
|
||||
)
|
||||
disposables.push(
|
||||
editCodeService.onDidChangeURIStreamState(({ uri }) => {
|
||||
const isStreaming = editCodeService.getURIStreamState({ uri })
|
||||
uriStreamingStateListeners.forEach(l => l(uri, isStreaming))
|
||||
})
|
||||
)
|
||||
|
||||
|
||||
|
||||
return disposables
|
||||
}
|
||||
|
|
@ -336,7 +353,21 @@ export const useRefreshModelListener = (listener: (providerName: RefreshableProv
|
|||
useEffect(() => {
|
||||
refreshModelProviderListeners.add(listener)
|
||||
return () => { refreshModelProviderListeners.delete(listener) }
|
||||
}, [listener])
|
||||
}, [listener, refreshModelProviderListeners])
|
||||
}
|
||||
|
||||
export const useCtrlKZoneStreamingState = (listener: (diffareaid: number, s: boolean) => void) => {
|
||||
useEffect(() => {
|
||||
ctrlKZoneStreamingStateListeners.add(listener)
|
||||
return () => { ctrlKZoneStreamingStateListeners.delete(listener) }
|
||||
}, [listener, ctrlKZoneStreamingStateListeners])
|
||||
}
|
||||
|
||||
export const useURIStreamState = (listener: (uri: URI, s: URIStreamState) => void) => {
|
||||
useEffect(() => {
|
||||
uriStreamingStateListeners.add(listener)
|
||||
return () => { uriStreamingStateListeners.delete(listener) }
|
||||
}, [listener, uriStreamingStateListeners])
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -353,3 +384,4 @@ export const useIsDark = () => {
|
|||
return isDark
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js'
|
||||
import { ProviderName, SettingName, displayInfoOfSettingName, providerNames, VoidModelInfo, globalSettingNames, customSettingNamesOfProvider, RefreshableProviderName, refreshableProviderNames, displayInfoOfProviderName, defaultProviderSettings, nonlocalProviderNames, localProviderNames, GlobalSettingName, featureNames, displayInfoOfFeatureName, isProviderNameDisabled } from '../../../../common/voidSettingsTypes.js'
|
||||
import { ProviderName, SettingName, displayInfoOfSettingName, providerNames, VoidModelInfo, globalSettingNames, customSettingNamesOfProvider, RefreshableProviderName, refreshableProviderNames, displayInfoOfProviderName, defaultProviderSettings, nonlocalProviderNames, localProviderNames, GlobalSettingName, featureNames, displayInfoOfFeatureName, isProviderNameDisabled, FeatureName } from '../../../../common/voidSettingsTypes.js'
|
||||
import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js'
|
||||
import { VoidButton, VoidCheckBox, VoidCustomDropdownBox, VoidInputBox, VoidInputBox2, VoidSwitch } from '../util/inputs.js'
|
||||
import { useAccessor, useIsDark, useRefreshModelListener, useRefreshModelState, useSettingsState } from '../util/services.js'
|
||||
|
|
@ -21,7 +21,7 @@ import { os } from '../../../helpers/systemInfo.js'
|
|||
|
||||
const SubtleButton = ({ onClick, text, icon, disabled }: { onClick: () => void, text: string, icon: React.ReactNode, disabled: boolean }) => {
|
||||
|
||||
return <div className='flex items-center text-void-fg-3 mb-1 px-3 rounded-sm overflow-hidden gap-2 hover:bg-black/10 dark:hover:bg-gray-300/10'>
|
||||
return <div className='flex items-center text-void-fg-3 px-3 py-0.5 rounded-sm overflow-hidden gap-2 hover:bg-black/10 dark:hover:bg-gray-300/10'>
|
||||
<button className='flex items-center' disabled={disabled} onClick={onClick}>
|
||||
{icon}
|
||||
</button>
|
||||
|
|
@ -82,9 +82,7 @@ const RefreshableModels = () => {
|
|||
|
||||
const buttons = refreshableProviderNames.map(providerName => {
|
||||
if (!settingsState.settingsOfProvider[providerName]._didFillInProviderSettings) return null
|
||||
return <div key={providerName} className='pb-4'>
|
||||
<RefreshModelButton providerName={providerName} />
|
||||
</div>
|
||||
return <RefreshModelButton key={providerName} providerName={providerName} />
|
||||
})
|
||||
|
||||
return <>
|
||||
|
|
@ -257,7 +255,7 @@ const ProviderSetting = ({ providerName, settingName }: { providerName: Provider
|
|||
|
||||
// const { title: providerTitle, } = displayInfoOfProviderName(providerName)
|
||||
|
||||
const { title: settingTitle, placeholder, subTextMd } = displayInfoOfSettingName(providerName, settingName)
|
||||
const { title: settingTitle, placeholder, isPasswordField, subTextMd } = displayInfoOfSettingName(providerName, settingName)
|
||||
|
||||
const accessor = useAccessor()
|
||||
const voidSettingsService = accessor.get('IVoidSettingsService')
|
||||
|
|
@ -269,6 +267,7 @@ const ProviderSetting = ({ providerName, settingName }: { providerName: Provider
|
|||
<VoidInputBox
|
||||
// placeholder={`${providerTitle} ${settingTitle} (${placeholder})`}
|
||||
placeholder={`${settingTitle} (${placeholder})`}
|
||||
|
||||
onChangeText={useCallback((newVal) => {
|
||||
if (weChangedTextRef) return
|
||||
voidSettingsService.setSettingOfProvider(providerName, settingName, newVal)
|
||||
|
|
@ -291,6 +290,7 @@ const ProviderSetting = ({ providerName, settingName }: { providerName: Provider
|
|||
return [disposable]
|
||||
}, [voidSettingsService, providerName, settingName])}
|
||||
multiline={false}
|
||||
isPasswordField={isPasswordField}
|
||||
/>
|
||||
{subTextMd === undefined ? null : <div className='py-1 px-3 opacity-50 text-sm'>
|
||||
<ChatMarkdownRender noSpace string={subTextMd} />
|
||||
|
|
@ -339,7 +339,7 @@ const SettingsForProvider = ({ providerName }: { providerName: ProviderName }) =
|
|||
{needsModel ?
|
||||
providerName === 'ollama' ?
|
||||
<WarningBox text={`Please install an Ollama model. We'll auto-detect it.`} />
|
||||
: <WarningBox text={`Please add a model for ${providerTitle} below (Models).`} />
|
||||
: <WarningBox text={`Please add a model for ${providerTitle} (Models section).`} />
|
||||
: null}
|
||||
</div>
|
||||
</div >
|
||||
|
|
@ -377,6 +377,7 @@ export const AutoRefreshToggle = () => {
|
|||
icon={enabled ? <Check className='stroke-green-500 size-3' /> : <X className='stroke-red-500 size-3' />}
|
||||
disabled={false}
|
||||
/>
|
||||
|
||||
}
|
||||
|
||||
export const AIInstructionsBox = () => {
|
||||
|
|
@ -400,6 +401,7 @@ export const FeaturesTab = () => {
|
|||
<ErrorBoundary>
|
||||
<AutoRefreshToggle />
|
||||
<RefreshableModels />
|
||||
<div className='py-2' />
|
||||
<ModelDump />
|
||||
<AddModelMenuFull />
|
||||
</ErrorBoundary>
|
||||
|
|
@ -413,7 +415,7 @@ export const FeaturesTab = () => {
|
|||
<div className='pl-4 opacity-50'>
|
||||
<span className={`text-sm mb-2`}><ChatMarkdownRender noSpace string={`1. Download [Ollama](https://ollama.com/download).`} /></span>
|
||||
<span className={`text-sm mb-2`}><ChatMarkdownRender noSpace string={`2. Open your terminal.`} /></span>
|
||||
<span className={`text-sm mb-2 select-text`}><ChatMarkdownRender noSpace string={`3. Run \`ollama run llama3.1\`. This installs Meta's llama3.1 model which is best for chat and inline edits. Requires 5GB of memory.`} /></span>
|
||||
<span className={`text-sm mb-2 select-text`}><ChatMarkdownRender noSpace string={`3. Run \`ollama run llama3.1:8b\`. This installs Meta's llama3.1 model which is best for chat and inline edits. Requires 5GB of memory.`} /></span>
|
||||
<span className={`text-sm mb-2 select-text`}><ChatMarkdownRender noSpace string={`4. Run \`ollama run qwen2.5-coder:1.5b\`. This installs a faster autocomplete model. Requires 1GB of memory.`} /></span>
|
||||
<span className={`text-sm mb-2`}><ChatMarkdownRender noSpace string={`Void automatically detects locally running models and enables them.`} /></span>
|
||||
{/* TODO we should create UI for downloading models without user going into terminal */}
|
||||
|
|
@ -435,12 +437,13 @@ export const FeaturesTab = () => {
|
|||
<h2 className={`text-3xl mb-2 mt-12`}>Feature Options</h2>
|
||||
<ErrorBoundary>
|
||||
{featureNames.map(featureName =>
|
||||
<div key={featureName}
|
||||
className='mb-2'
|
||||
>
|
||||
<h4 className={`text-void-fg-3`}>{displayInfoOfFeatureName(featureName)}</h4>
|
||||
<ModelDropdown featureName={featureName} />
|
||||
</div>
|
||||
(['Ctrl+L', 'Ctrl+K'] as FeatureName[]).includes(featureName) ? null :
|
||||
<div key={featureName}
|
||||
className='mb-2'
|
||||
>
|
||||
<h4 className={`text-void-fg-3`}>{displayInfoOfFeatureName(featureName)}</h4>
|
||||
<ModelDropdown featureName={featureName} />
|
||||
</div>
|
||||
)}
|
||||
</ErrorBoundary>
|
||||
|
||||
|
|
@ -624,7 +627,7 @@ export const Settings = () => {
|
|||
|
||||
<div className='max-w-5xl mx-auto'>
|
||||
|
||||
<h1 className='text-2xl w-full'>Void Settings</h1>
|
||||
<h1 className='text-2xl w-full'>{`Void's Settings`}</h1>
|
||||
|
||||
{/* separator */}
|
||||
<div className='w-full h-[1px] my-4' />
|
||||
|
|
|
|||
|
|
@ -28,17 +28,25 @@ module.exports = {
|
|||
|
||||
colors: {
|
||||
"void-bg-1": "var(--vscode-input-background)",
|
||||
"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-sideBar-background) 30%, var(--vscode-editor-background) 70%)",
|
||||
"void-bg-3": "var(--vscode-editor-background)",
|
||||
|
||||
|
||||
"void-fg-1": "var(--vscode-editor-foreground)",
|
||||
"void-fg-2": "var(--vscode-input-foreground)",
|
||||
"void-fg-3": "var(--vscode-input-placeholderForeground)",
|
||||
// "void-fg-4": "var(--vscode-tab-inactiveForeground)",
|
||||
"void-fg-4": "var(--vscode-list-deemphasizedForeground)",
|
||||
|
||||
|
||||
"void-warning": "var(--vscode-charts-yellow)",
|
||||
|
||||
"void-border-1": "var(--vscode-commandCenter-activeBorder)",
|
||||
"void-border-2": "var(--vscode-commandCenter-border)",
|
||||
"void-border-3": "var(--vscode-commandCenter-inactiveBorder)",
|
||||
"void-border-3": "var(--vscode-settings-sashBorder)",
|
||||
|
||||
|
||||
vscode: {
|
||||
|
|
|
|||
|
|
@ -141,13 +141,11 @@ registerAction2(class extends Action2 {
|
|||
let setSelections = (s: StagingSelectionItem[]) => { }
|
||||
|
||||
if (focusedMessageIdx === undefined) {
|
||||
const [state, setState] = chatThreadService._useCurrentThreadState()
|
||||
selections = state.stagingSelections
|
||||
setSelections = (s) => setState({ stagingSelections: s })
|
||||
selections = chatThreadService.getCurrentThreadStagingSelections()
|
||||
setSelections = (s: StagingSelectionItem[]) => chatThreadService.setCurrentThreadStagingSelections(s)
|
||||
} else {
|
||||
const [state, setState] = chatThreadService._useCurrentMessageState(focusedMessageIdx)
|
||||
selections = state.stagingSelections
|
||||
setSelections = (s) => setState({ stagingSelections: s })
|
||||
selections = chatThreadService.getCurrentMessageState(focusedMessageIdx).stagingSelections
|
||||
setSelections = (s) => chatThreadService.setCurrentMessageState(focusedMessageIdx, { stagingSelections: s })
|
||||
}
|
||||
|
||||
// if matches with existing selection, overwrite (since text may change)
|
||||
|
|
@ -241,7 +239,7 @@ registerAction2(class extends Action2 {
|
|||
constructor() {
|
||||
super({
|
||||
id: 'void.settingsAction',
|
||||
title: 'Void Settings',
|
||||
title: `Void's Settings`,
|
||||
icon: { id: 'settings-gear' },
|
||||
menu: [{ id: MenuId.ViewTitle, group: 'navigation', when: ContextKeyExpr.equals('view', VOID_VIEW_ID), }]
|
||||
});
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ class VoidSettingsInput extends EditorInput {
|
|||
}
|
||||
|
||||
override getName(): string {
|
||||
return nls.localize('voidSettingsInputsName', 'Void Settings');
|
||||
return nls.localize('voidSettingsInputsName', 'Void\'s Settings');
|
||||
}
|
||||
|
||||
override getIcon() {
|
||||
|
|
@ -112,7 +112,7 @@ class VoidSettingsPane extends EditorPane {
|
|||
|
||||
// register Settings pane
|
||||
Registry.as<IEditorPaneRegistry>(EditorExtensions.EditorPane).registerEditorPane(
|
||||
EditorPaneDescriptor.create(VoidSettingsPane, VoidSettingsPane.ID, nls.localize('VoidSettingsPane', "Void Settings Pane")),
|
||||
EditorPaneDescriptor.create(VoidSettingsPane, VoidSettingsPane.ID, nls.localize('VoidSettingsPane', "Void\'s Settings Pane")),
|
||||
[new SyncDescriptor(VoidSettingsInput)]
|
||||
);
|
||||
|
||||
|
|
@ -202,7 +202,7 @@ MenuRegistry.appendMenuItem(MenuId.GlobalActivity, {
|
|||
group: '0_command',
|
||||
command: {
|
||||
id: VOID_TOGGLE_SETTINGS_ACTION_ID,
|
||||
title: nls.localize('voidSettings', "Void Settings")
|
||||
title: nls.localize('voidSettings', "Void\'s Settings")
|
||||
},
|
||||
order: 1
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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, OpenaiCompatibleModelResponse, } from './llmMessageTypes.js';
|
||||
import { EventLLMMessageOnTextParams, EventLLMMessageOnErrorParams, EventLLMMessageOnFinalMessageParams, ServiceSendLLMMessageParams, MainSendLLMMessageParams, MainLLMMessageAbortParams, ServiceModelListParams, EventModelListOnSuccessParams, EventModelListOnErrorParams, MainModelListParams, OllamaModelResponse, VLLMModelResponse, } from './llmMessageTypes.js';
|
||||
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
|
||||
|
|
@ -24,27 +24,39 @@ export interface ILLMMessageService {
|
|||
sendLLMMessage: (params: ServiceSendLLMMessageParams) => string | null;
|
||||
abort: (requestId: string) => void;
|
||||
ollamaList: (params: ServiceModelListParams<OllamaModelResponse>) => void;
|
||||
openAICompatibleList: (params: ServiceModelListParams<OpenaiCompatibleModelResponse>) => void;
|
||||
vLLMList: (params: ServiceModelListParams<VLLMModelResponse>) => void;
|
||||
}
|
||||
|
||||
|
||||
// open this file side by side with llmMessageChannel
|
||||
export class LLMMessageService extends Disposable implements ILLMMessageService {
|
||||
|
||||
readonly _serviceBrand: undefined;
|
||||
private readonly channel: IChannel // LLMMessageChannel
|
||||
|
||||
// llmMessage
|
||||
private readonly onTextHooks_llm: { [eventId: string]: ((params: EventLLMMessageOnTextParams) => void) } = {}
|
||||
private readonly onFinalMessageHooks_llm: { [eventId: string]: ((params: EventLLMMessageOnFinalMessageParams) => void) } = {}
|
||||
private readonly onErrorHooks_llm: { [eventId: string]: ((params: EventLLMMessageOnErrorParams) => void) } = {}
|
||||
// sendLLMMessage
|
||||
private readonly llmMessageHooks = {
|
||||
onText: {} as { [eventId: string]: ((params: EventLLMMessageOnTextParams) => void) },
|
||||
onFinalMessage: {} as { [eventId: string]: ((params: EventLLMMessageOnFinalMessageParams) => void) },
|
||||
onError: {} as { [eventId: string]: ((params: EventLLMMessageOnErrorParams) => void) },
|
||||
}
|
||||
|
||||
|
||||
// ollamaList
|
||||
private readonly onSuccess_ollama: { [eventId: string]: ((params: EventModelListOnSuccessParams<OllamaModelResponse>) => void) } = {}
|
||||
private readonly onError_ollama: { [eventId: string]: ((params: EventModelListOnErrorParams<OllamaModelResponse>) => void) } = {}
|
||||
|
||||
// openAICompatibleList
|
||||
private readonly onSuccess_openAICompatible: { [eventId: string]: ((params: EventModelListOnSuccessParams<OpenaiCompatibleModelResponse>) => void) } = {}
|
||||
private readonly onError_openAICompatible: { [eventId: string]: ((params: EventModelListOnErrorParams<OpenaiCompatibleModelResponse>) => void) } = {}
|
||||
// list hooks
|
||||
private readonly listHooks = {
|
||||
ollama: {
|
||||
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) },
|
||||
}
|
||||
} satisfies {
|
||||
[providerName: string]: {
|
||||
success: { [eventId: string]: ((params: EventModelListOnSuccessParams<any>) => void) },
|
||||
error: { [eventId: string]: ((params: EventModelListOnErrorParams<any>) => void) },
|
||||
}
|
||||
}
|
||||
|
||||
constructor(
|
||||
@IMainProcessService private readonly mainProcessService: IMainProcessService, // used as a renderer (only usable on client side)
|
||||
|
|
@ -59,32 +71,14 @@ 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_llm') satisfies Event<EventLLMMessageOnTextParams>)(e => {
|
||||
this.onTextHooks_llm[e.requestId]?.(e)
|
||||
}))
|
||||
this._register((this.channel.listen('onFinalMessage_llm') satisfies Event<EventLLMMessageOnFinalMessageParams>)(e => {
|
||||
this.onFinalMessageHooks_llm[e.requestId]?.(e)
|
||||
this._onRequestIdDone(e.requestId)
|
||||
}))
|
||||
this._register((this.channel.listen('onError_llm') satisfies Event<EventLLMMessageOnErrorParams>)(e => {
|
||||
console.error('Error in LLMMessageService:', JSON.stringify(e))
|
||||
this.onErrorHooks_llm[e.requestId]?.(e)
|
||||
this._onRequestIdDone(e.requestId)
|
||||
}))
|
||||
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._onRequestIdDone(e.requestId) }))
|
||||
this._register((this.channel.listen('onError_sendLLMMessage') satisfies Event<EventLLMMessageOnErrorParams>)(e => { this.llmMessageHooks.onError[e.requestId]?.(e); this._onRequestIdDone(e.requestId); console.error('Error in LLMMessageService:', JSON.stringify(e)) }))
|
||||
// ollama .list()
|
||||
this._register((this.channel.listen('onSuccess_ollama') satisfies Event<EventModelListOnSuccessParams<OllamaModelResponse>>)(e => {
|
||||
this.onSuccess_ollama[e.requestId]?.(e)
|
||||
}))
|
||||
this._register((this.channel.listen('onError_ollama') satisfies Event<EventModelListOnErrorParams<OllamaModelResponse>>)(e => {
|
||||
this.onError_ollama[e.requestId]?.(e)
|
||||
}))
|
||||
// openaiCompatible .list()
|
||||
this._register((this.channel.listen('onSuccess_openAICompatible') satisfies Event<EventModelListOnSuccessParams<OpenaiCompatibleModelResponse>>)(e => {
|
||||
this.onSuccess_openAICompatible[e.requestId]?.(e)
|
||||
}))
|
||||
this._register((this.channel.listen('onError_openAICompatible') satisfies Event<EventModelListOnErrorParams<OpenaiCompatibleModelResponse>>)(e => {
|
||||
this.onError_openAICompatible[e.requestId]?.(e)
|
||||
}))
|
||||
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) }))
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -99,15 +93,15 @@ export class LLMMessageService extends Disposable implements ILLMMessageService
|
|||
let message: string
|
||||
|
||||
if (isDisabled === 'addProvider' || isDisabled === 'providerNotAutoDetected')
|
||||
message = `Please add a provider in Void Settings.`
|
||||
message = `Please add a provider in Void's Settings.`
|
||||
else if (isDisabled === 'addModel')
|
||||
message = `Please add a model.`
|
||||
else if (isDisabled === 'needToEnableModel')
|
||||
message = `Please enable a model.`
|
||||
else if (isDisabled === 'notFilledIn')
|
||||
message = `Please fill in Void Settings${modelSelection !== null ? ` for ${displayInfoOfProviderName(modelSelection.providerName).title}` : ''}.`
|
||||
message = `Please fill in Void's Settings${modelSelection !== null ? ` for ${displayInfoOfProviderName(modelSelection.providerName).title}` : ''}.`
|
||||
else
|
||||
message = 'Please add a provider in Void Settings.'
|
||||
message = `Please add a provider in Void's Settings.`
|
||||
|
||||
onError({ message, fullError: null })
|
||||
return null
|
||||
|
|
@ -117,9 +111,9 @@ export class LLMMessageService extends Disposable implements ILLMMessageService
|
|||
|
||||
// add state for request id
|
||||
const requestId = generateUuid();
|
||||
this.onTextHooks_llm[requestId] = onText
|
||||
this.onFinalMessageHooks_llm[requestId] = onFinalMessage
|
||||
this.onErrorHooks_llm[requestId] = onError
|
||||
this.llmMessageHooks.onText[requestId] = onText
|
||||
this.llmMessageHooks.onFinalMessage[requestId] = onFinalMessage
|
||||
this.llmMessageHooks.onError[requestId] = onError
|
||||
|
||||
const { aiInstructions } = this.voidSettingsService.state.globalSettings
|
||||
const { settingsOfProvider } = this.voidSettingsService.state
|
||||
|
|
@ -151,43 +145,46 @@ export class LLMMessageService extends Disposable implements ILLMMessageService
|
|||
|
||||
// add state for request id
|
||||
const requestId_ = generateUuid();
|
||||
this.onSuccess_ollama[requestId_] = onSuccess
|
||||
this.onError_ollama[requestId_] = onError
|
||||
this.listHooks.ollama.success[requestId_] = onSuccess
|
||||
this.listHooks.ollama.error[requestId_] = onError
|
||||
|
||||
this.channel.call('ollamaList', {
|
||||
...proxyParams,
|
||||
settingsOfProvider,
|
||||
providerName: 'ollama',
|
||||
requestId: requestId_,
|
||||
} satisfies MainModelListParams<OllamaModelResponse>)
|
||||
}
|
||||
|
||||
|
||||
openAICompatibleList = (params: ServiceModelListParams<OpenaiCompatibleModelResponse>) => {
|
||||
vLLMList = (params: ServiceModelListParams<VLLMModelResponse>) => {
|
||||
const { onSuccess, onError, ...proxyParams } = params
|
||||
|
||||
const { settingsOfProvider } = this.voidSettingsService.state
|
||||
|
||||
// add state for request id
|
||||
const requestId_ = generateUuid();
|
||||
this.onSuccess_openAICompatible[requestId_] = onSuccess
|
||||
this.onError_openAICompatible[requestId_] = onError
|
||||
this.listHooks.vLLM.success[requestId_] = onSuccess
|
||||
this.listHooks.vLLM.error[requestId_] = onError
|
||||
|
||||
this.channel.call('openAICompatibleList', {
|
||||
this.channel.call('vLLMList', {
|
||||
...proxyParams,
|
||||
settingsOfProvider,
|
||||
providerName: 'vLLM',
|
||||
requestId: requestId_,
|
||||
} satisfies MainModelListParams<OpenaiCompatibleModelResponse>)
|
||||
} satisfies MainModelListParams<VLLMModelResponse>)
|
||||
}
|
||||
|
||||
|
||||
|
||||
_onRequestIdDone(requestId: string) {
|
||||
delete this.onTextHooks_llm[requestId]
|
||||
delete this.onFinalMessageHooks_llm[requestId]
|
||||
delete this.onErrorHooks_llm[requestId]
|
||||
delete this.llmMessageHooks.onText[requestId]
|
||||
delete this.llmMessageHooks.onFinalMessage[requestId]
|
||||
delete this.llmMessageHooks.onError[requestId]
|
||||
|
||||
delete this.onSuccess_ollama[requestId]
|
||||
delete this.onError_ollama[requestId]
|
||||
delete this.listHooks.ollama.success[requestId]
|
||||
delete this.listHooks.ollama.error[requestId]
|
||||
|
||||
delete this.listHooks.vLLM.success[requestId]
|
||||
delete this.listHooks.vLLM.error[requestId]
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ export type ToolCallType = {
|
|||
}
|
||||
|
||||
|
||||
export type OnText = (p: { newText: string, fullText: string }) => void
|
||||
export type OnText = (p: { newText: string, fullText: string; newReasoning: string; fullReasoning: string }) => void
|
||||
export type OnFinalMessage = (p: { fullText: string, toolCalls?: ToolCallType[] }) => void // id is tool_use_id
|
||||
export type OnError = (p: { message: string, fullError: Error | null }) => void
|
||||
export type AbortRef = { current: (() => void) | null }
|
||||
|
|
@ -65,7 +65,7 @@ export const toLLMChatMessage = (c: ChatMessage): LLMChatMessage => {
|
|||
}
|
||||
|
||||
|
||||
type _InternalSendFIMMessage = {
|
||||
export type LLMFIMMessage = {
|
||||
prefix: string;
|
||||
suffix: string;
|
||||
stopTokens: string[];
|
||||
|
|
@ -77,7 +77,7 @@ type SendLLMType = {
|
|||
tools?: InternalToolInfo[];
|
||||
} | {
|
||||
messagesType: 'FIMMessage';
|
||||
messages: _InternalSendFIMMessage;
|
||||
messages: LLMFIMMessage;
|
||||
tools?: undefined;
|
||||
}
|
||||
|
||||
|
|
@ -118,38 +118,6 @@ export type EventLLMMessageOnFinalMessageParams = Parameters<OnFinalMessage>[0]
|
|||
export type EventLLMMessageOnErrorParams = Parameters<OnError>[0] & { requestId: string }
|
||||
|
||||
|
||||
export type _InternalSendLLMChatMessageFnType = (
|
||||
params: {
|
||||
aiInstructions: string;
|
||||
|
||||
onText: OnText;
|
||||
onFinalMessage: OnFinalMessage;
|
||||
onError: OnError;
|
||||
providerName: ProviderName;
|
||||
settingsOfProvider: SettingsOfProvider;
|
||||
modelName: string;
|
||||
_setAborter: (aborter: () => void) => void;
|
||||
|
||||
tools?: InternalToolInfo[],
|
||||
|
||||
messages: LLMChatMessage[];
|
||||
}
|
||||
) => void
|
||||
|
||||
export type _InternalSendLLMFIMMessageFnType = (
|
||||
params: {
|
||||
onText: OnText;
|
||||
onFinalMessage: OnFinalMessage;
|
||||
onError: OnError;
|
||||
providerName: ProviderName;
|
||||
settingsOfProvider: SettingsOfProvider;
|
||||
modelName: string;
|
||||
_setAborter: (aborter: () => void) => void;
|
||||
|
||||
messages: _InternalSendFIMMessage;
|
||||
}
|
||||
) => void
|
||||
|
||||
// service -> main -> internal -> event (back to main)
|
||||
// (browser)
|
||||
|
||||
|
|
@ -181,18 +149,22 @@ export type OllamaModelResponse = {
|
|||
size_vram: number;
|
||||
}
|
||||
|
||||
export type OpenaiCompatibleModelResponse = {
|
||||
type OpenaiCompatibleModelResponse = {
|
||||
id: string;
|
||||
created: number;
|
||||
object: 'model';
|
||||
owned_by: string;
|
||||
}
|
||||
|
||||
export type VLLMModelResponse = OpenaiCompatibleModelResponse
|
||||
|
||||
|
||||
|
||||
// params to the true list fn
|
||||
export type ModelListParams<modelResponse> = {
|
||||
export type ModelListParams<ModelResponse> = {
|
||||
providerName: ProviderName;
|
||||
settingsOfProvider: SettingsOfProvider;
|
||||
onSuccess: (param: { models: modelResponse[] }) => void;
|
||||
onSuccess: (param: { models: ModelResponse[] }) => void;
|
||||
onError: (param: { error: string }) => void;
|
||||
}
|
||||
|
||||
|
|
@ -211,4 +183,3 @@ export type EventModelListOnErrorParams<modelResponse> = Parameters<ModelListPar
|
|||
|
||||
|
||||
|
||||
export type _InternalModelListFnType<modelResponse> = (params: ModelListParams<modelResponse>) => void
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { ILLMMessageService } from './llmMessageService.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, OpenaiCompatibleModelResponse } from './llmMessageTypes.js';
|
||||
import { OllamaModelResponse, VLLMModelResponse } from './llmMessageTypes.js';
|
||||
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
|
||||
|
|
@ -160,9 +160,8 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
|
|||
}
|
||||
}
|
||||
const listFn = providerName === 'ollama' ? this.llmMessageService.ollamaList
|
||||
: providerName === 'vLLM' ? this.llmMessageService.openAICompatibleList
|
||||
: providerName === 'openAICompatible' ? this.llmMessageService.openAICompatibleList
|
||||
: () => { }
|
||||
: providerName === 'vLLM' ? this.llmMessageService.vLLMList
|
||||
: () => { }
|
||||
|
||||
listFn({
|
||||
onSuccess: ({ models }) => {
|
||||
|
|
@ -172,8 +171,7 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
|
|||
providerName,
|
||||
models.map(model => {
|
||||
if (providerName === 'ollama') return (model as OllamaModelResponse).name;
|
||||
else if (providerName === 'vLLM') return (model as OpenaiCompatibleModelResponse).id;
|
||||
else if (providerName === 'openAICompatible') return (model as OpenaiCompatibleModelResponse).id;
|
||||
else if (providerName === 'vLLM') return (model as VLLMModelResponse).id;
|
||||
else throw new Error('refreshMode fn: unknown provider', providerName);
|
||||
}),
|
||||
{ enableProviderOnSuccess: options.enableProviderOnSuccess, hideRefresh: options.doNotFire }
|
||||
|
|
|
|||
|
|
@ -1,13 +1,12 @@
|
|||
import { CancellationToken } from '../../../../base/common/cancellation.js'
|
||||
import { URI } from '../../../../base/common/uri.js'
|
||||
import { IModelService } from '../../../../editor/common/services/model.js'
|
||||
import { IFileService } from '../../../../platform/files/common/files.js'
|
||||
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js'
|
||||
import { createDecorator, IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'
|
||||
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'
|
||||
import { VSReadFile } from '../../../../workbench/contrib/void/browser/helpers/readFile.js'
|
||||
import { QueryBuilder } from '../../../../workbench/services/search/common/queryBuilder.js'
|
||||
import { ISearchService } from '../../../../workbench/services/search/common/search.js'
|
||||
import { IVoidFileService } from './voidFileService.js'
|
||||
|
||||
|
||||
// tool use for AI
|
||||
|
|
@ -24,7 +23,6 @@ export type InternalToolInfo = {
|
|||
required: string[], // required paramNames
|
||||
}
|
||||
|
||||
// helper
|
||||
const paginationHelper = {
|
||||
desc: `Very large results may be paginated (indicated in the result). Pagination fails gracefully if out of bounds or invalid page number.`,
|
||||
param: { pageNumber: { type: 'number', description: 'The page number (optional, default is 1).' }, }
|
||||
|
|
@ -33,7 +31,7 @@ const paginationHelper = {
|
|||
export const voidTools = {
|
||||
read_file: {
|
||||
name: 'read_file',
|
||||
description: 'Returns file contents of a given URI.',
|
||||
description: `Returns file contents of a given URI. ${paginationHelper.desc}`,
|
||||
params: {
|
||||
uri: { type: 'string', description: undefined },
|
||||
},
|
||||
|
|
@ -57,7 +55,7 @@ export const voidTools = {
|
|||
query: { type: 'string', description: undefined },
|
||||
...paginationHelper.param,
|
||||
},
|
||||
required: ['query']
|
||||
required: ['query'],
|
||||
},
|
||||
|
||||
search: {
|
||||
|
|
@ -70,6 +68,18 @@ export const voidTools = {
|
|||
required: ['query'],
|
||||
},
|
||||
|
||||
// go_to_definition:
|
||||
|
||||
// go_to_usages:
|
||||
|
||||
// create_file: {
|
||||
// name: 'create_file',
|
||||
// description: `Creates a file at the given path. Fails gracefully if the file already exists by doing nothing.`
|
||||
// params: {
|
||||
// uri: { type: 'string', description: undefined },
|
||||
// }
|
||||
// }
|
||||
|
||||
// semantic_search: {
|
||||
// description: 'Searches files semantically for the given string query.',
|
||||
// // RAG
|
||||
|
|
@ -79,69 +89,103 @@ export const voidTools = {
|
|||
export type ToolName = keyof typeof voidTools
|
||||
export const toolNames = Object.keys(voidTools) as ToolName[]
|
||||
|
||||
const toolNamesSet = new Set<string>(toolNames)
|
||||
export const isAToolName = (toolName: string): toolName is ToolName => {
|
||||
const isAToolName = toolNamesSet.has(toolName)
|
||||
return isAToolName
|
||||
}
|
||||
|
||||
|
||||
export type ToolParamNames<T extends ToolName> = keyof typeof voidTools[T]['params']
|
||||
export type ToolParamsObj<T extends ToolName> = { [paramName in ToolParamNames<T>]: unknown }
|
||||
|
||||
export type ToolCallReturnType = {
|
||||
'read_file': { uri: URI, fileContents: string, hasNextPage: boolean },
|
||||
'list_dir': { rootURI: URI, children: DirectoryItem[] | null, hasNextPage: boolean, hasPrevPage: boolean, itemsRemaining: number },
|
||||
'pathname_search': { queryStr: string, uris: URI[] | string, hasNextPage: boolean },
|
||||
'search': { queryStr: string, uris: URI[] | string, hasNextPage: boolean }
|
||||
'create_file': {}
|
||||
}
|
||||
|
||||
export type ToolCallReturnType<T extends ToolName>
|
||||
= T extends 'read_file' ? string
|
||||
: T extends 'list_dir' ? string
|
||||
: T extends 'pathname_search' ? string | URI[]
|
||||
: T extends 'search' ? string | URI[]
|
||||
: never
|
||||
type DirectoryItem = {
|
||||
name: string;
|
||||
isDirectory: boolean;
|
||||
isSymbolicLink: boolean;
|
||||
}
|
||||
|
||||
export type ToolFns = { [T in ToolName]: (p: string) => Promise<[ToolCallReturnType<T>, boolean]> }
|
||||
export type ToolResultToString = { [T in ToolName]: (result: [ToolCallReturnType<T>, boolean]) => string }
|
||||
export type ToolFns = { [T in ToolName]: (p: string) => Promise<ToolCallReturnType[T]> }
|
||||
export type ToolResultToString = { [T in ToolName]: (result: ToolCallReturnType[T]) => string }
|
||||
|
||||
|
||||
// pagination info
|
||||
const MAX_FILE_CHARS_PAGE = 50_000
|
||||
const MAX_CHILDREN_URIs_PAGE = 500
|
||||
|
||||
const MAX_DEPTH = 1
|
||||
async function generateDirectoryTreeMd(fileService: IFileService, rootURI: URI, pageNumber: number): Promise<[string, boolean]> {
|
||||
let output = '';
|
||||
|
||||
const indentation = (depth: number, isLast: boolean): string => {
|
||||
if (depth === 0) return '';
|
||||
return `${'| '.repeat(depth - 1)}${isLast ? '└── ' : '├── '}`;
|
||||
};
|
||||
|
||||
let hasNextPage = false
|
||||
|
||||
async function traverseChildren(uri: URI, depth: number, isLast: boolean) {
|
||||
const stat = await fileService.resolve(uri, { resolveMetadata: false });
|
||||
|
||||
// we might want to say where symlink links to
|
||||
if ((depth === 0 && pageNumber === 1) || depth !== 0)
|
||||
output += `${indentation(depth, isLast)}${stat.name}${stat.isDirectory ? '/' : ''}${stat.isSymbolicLink ? ` (symbolic link)` : ''}\n`;
|
||||
|
||||
// list children
|
||||
const originalChildrenLength = stat.children?.length ?? 0
|
||||
const fromChildIdx = MAX_CHILDREN_URIs_PAGE * (pageNumber - 1)
|
||||
const toChildIdx = MAX_CHILDREN_URIs_PAGE * pageNumber - 1 // INCLUSIVE
|
||||
const listChildren = stat.children?.slice(fromChildIdx, toChildIdx + 1) ?? [];
|
||||
|
||||
if (!stat.isDirectory) return;
|
||||
|
||||
if (listChildren.length === 0) return
|
||||
if (depth === MAX_DEPTH) return // right now MAX_DEPTH=1 to make pagination work nicely
|
||||
|
||||
for (let i = 0; i < Math.min(listChildren.length, MAX_CHILDREN_URIs_PAGE); i++) {
|
||||
await traverseChildren(listChildren[i].resource, depth + 1, i === listChildren.length - 1);
|
||||
}
|
||||
const nCutoffResults = (originalChildrenLength - 1) - toChildIdx
|
||||
if (nCutoffResults >= 1) {
|
||||
output += `${indentation(depth + 1, true)}(${nCutoffResults} results remaining...)\n`
|
||||
hasNextPage = true
|
||||
}
|
||||
|
||||
const computeDirectoryResult = async (
|
||||
fileService: IFileService,
|
||||
rootURI: URI,
|
||||
pageNumber: number = 1
|
||||
): Promise<ToolCallReturnType['list_dir']> => {
|
||||
const stat = await fileService.resolve(rootURI, { resolveMetadata: false });
|
||||
if (!stat.isDirectory) {
|
||||
return { rootURI, children: null, hasNextPage: false, hasPrevPage: false, itemsRemaining: 0 };
|
||||
}
|
||||
|
||||
await traverseChildren(rootURI, 0, false);
|
||||
const originalChildrenLength = stat.children?.length ?? 0;
|
||||
const fromChildIdx = MAX_CHILDREN_URIs_PAGE * (pageNumber - 1);
|
||||
const toChildIdx = MAX_CHILDREN_URIs_PAGE * pageNumber - 1; // INCLUSIVE
|
||||
const listChildren = stat.children?.slice(fromChildIdx, toChildIdx + 1) ?? [];
|
||||
|
||||
const children: DirectoryItem[] = listChildren.map(child => ({
|
||||
name: child.name,
|
||||
isDirectory: child.isDirectory,
|
||||
isSymbolicLink: child.isSymbolicLink || false
|
||||
}));
|
||||
|
||||
const hasNextPage = (originalChildrenLength - 1) > toChildIdx;
|
||||
const hasPrevPage = pageNumber > 1;
|
||||
const itemsRemaining = Math.max(0, originalChildrenLength - (toChildIdx + 1));
|
||||
|
||||
return {
|
||||
rootURI,
|
||||
children,
|
||||
hasNextPage,
|
||||
hasPrevPage,
|
||||
itemsRemaining
|
||||
};
|
||||
};
|
||||
|
||||
const directoryResultToString = (result: ToolCallReturnType['list_dir']): string => {
|
||||
if (!result.children) {
|
||||
return `Error: ${result.rootURI} is not a directory`;
|
||||
}
|
||||
|
||||
let output = '';
|
||||
const entries = result.children;
|
||||
|
||||
if (!result.hasPrevPage) {
|
||||
output += `${result.rootURI}\n`;
|
||||
}
|
||||
|
||||
for (let i = 0; i < entries.length; i++) {
|
||||
const entry = entries[i];
|
||||
const isLast = i === entries.length - 1 && !result.hasNextPage;
|
||||
const prefix = isLast ? '└── ' : '├── ';
|
||||
|
||||
output += `${prefix}${entry.name}${entry.isDirectory ? '/' : ''}${entry.isSymbolicLink ? ' (symbolic link)' : ''}\n`;
|
||||
}
|
||||
|
||||
if (result.hasNextPage) {
|
||||
output += `└── (${result.itemsRemaining} results remaining...)\n`;
|
||||
}
|
||||
|
||||
return output;
|
||||
};
|
||||
|
||||
|
||||
|
||||
return [output, hasNextPage]
|
||||
}
|
||||
|
||||
|
||||
const validateJSON = (s: string): { [s: string]: unknown } => {
|
||||
|
|
@ -162,8 +206,10 @@ const validateQueryStr = (queryStr: unknown) => {
|
|||
}
|
||||
|
||||
|
||||
// TODO!!!! check to make sure in workspace
|
||||
const validateURI = (uriStr: unknown) => {
|
||||
if (typeof uriStr !== 'string') throw new Error('Error calling tool: provided uri must be a string.')
|
||||
|
||||
const uri = URI.file(uriStr)
|
||||
return uri
|
||||
}
|
||||
|
|
@ -192,43 +238,52 @@ export class ToolsService implements IToolsService {
|
|||
|
||||
constructor(
|
||||
@IFileService fileService: IFileService,
|
||||
@IModelService modelService: IModelService,
|
||||
@IWorkspaceContextService workspaceContextService: IWorkspaceContextService,
|
||||
@ISearchService searchService: ISearchService,
|
||||
@IInstantiationService instantiationService: IInstantiationService,
|
||||
@IVoidFileService voidFileService: IVoidFileService,
|
||||
) {
|
||||
|
||||
const queryBuilder = instantiationService.createInstance(QueryBuilder);
|
||||
|
||||
this.toolFns = {
|
||||
read_file: async (s: string) => {
|
||||
console.log('read_file')
|
||||
|
||||
const o = validateJSON(s)
|
||||
const { uri: uriStr, pageNumber: pageNumberUnknown } = o
|
||||
|
||||
const uri = validateURI(uriStr)
|
||||
const pageNumber = validatePageNum(pageNumberUnknown)
|
||||
|
||||
const readFileContents = await VSReadFile(uri, modelService, fileService)
|
||||
const readFileContents = await voidFileService.readFile(uri)
|
||||
|
||||
const fromIdx = MAX_FILE_CHARS_PAGE * (pageNumber - 1)
|
||||
const toIdx = MAX_FILE_CHARS_PAGE * pageNumber - 1
|
||||
let fileContents = readFileContents.slice(fromIdx, toIdx + 1) // paginate
|
||||
const fileContents = readFileContents.slice(fromIdx, toIdx + 1) || '(empty)' // paginate
|
||||
const hasNextPage = (readFileContents.length - 1) - toIdx >= 1
|
||||
|
||||
return [fileContents || '(empty)', hasNextPage]
|
||||
|
||||
console.log('read_file result:', fileContents)
|
||||
|
||||
|
||||
return { uri, fileContents, hasNextPage }
|
||||
},
|
||||
list_dir: async (s: string) => {
|
||||
console.log('list_dir')
|
||||
const o = validateJSON(s)
|
||||
const { uri: uriStr, pageNumber: pageNumberUnknown } = o
|
||||
|
||||
const uri = validateURI(uriStr)
|
||||
const pageNumber = validatePageNum(pageNumberUnknown)
|
||||
|
||||
// TODO!!!! check to make sure in workspace
|
||||
const [treeStr, hasNextPage] = await generateDirectoryTreeMd(fileService, uri, pageNumber)
|
||||
return [treeStr, hasNextPage]
|
||||
const dirResult = await computeDirectoryResult(fileService, uri, pageNumber)
|
||||
console.log('list_dir result:', dirResult)
|
||||
|
||||
return dirResult
|
||||
},
|
||||
pathname_search: async (s: string) => {
|
||||
console.log('pathname_search')
|
||||
const o = validateJSON(s)
|
||||
const { query: queryUnknown, pageNumber: pageNumberUnknown } = o
|
||||
|
||||
|
|
@ -240,15 +295,20 @@ export class ToolsService implements IToolsService {
|
|||
|
||||
const fromIdx = MAX_CHILDREN_URIs_PAGE * (pageNumber - 1)
|
||||
const toIdx = MAX_CHILDREN_URIs_PAGE * pageNumber - 1
|
||||
const URIs = data.results
|
||||
const uris = data.results
|
||||
.slice(fromIdx, toIdx + 1) // paginate
|
||||
.map(({ resource, results }) => resource)
|
||||
|
||||
const hasNextPage = (data.results.length - 1) - toIdx >= 1
|
||||
console.log('pathname_search result:', uris)
|
||||
|
||||
return [URIs, hasNextPage]
|
||||
return { queryStr, uris, hasNextPage }
|
||||
},
|
||||
search: async (s: string) => {
|
||||
|
||||
|
||||
console.log('search')
|
||||
|
||||
const o = validateJSON(s)
|
||||
const { query: queryUnknown, pageNumber: pageNumberUnknown } = o
|
||||
|
||||
|
|
@ -260,34 +320,37 @@ export class ToolsService implements IToolsService {
|
|||
|
||||
const fromIdx = MAX_CHILDREN_URIs_PAGE * (pageNumber - 1)
|
||||
const toIdx = MAX_CHILDREN_URIs_PAGE * pageNumber - 1
|
||||
const URIs = data.results
|
||||
const uris = data.results
|
||||
.slice(fromIdx, toIdx + 1) // paginate
|
||||
.map(({ resource, results }) => resource)
|
||||
|
||||
const hasNextPage = (data.results.length - 1) - toIdx >= 1
|
||||
|
||||
return [URIs, hasNextPage]
|
||||
console.log('search result:', uris)
|
||||
|
||||
return { queryStr, uris, hasNextPage }
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const nextPageStr = (hasNextPage: boolean) => hasNextPage ? '\n\n(more on next page...)' : ''
|
||||
|
||||
this.toolResultToString = {
|
||||
read_file: ([fileContents, hasNextPage]) => {
|
||||
return fileContents + nextPageStr(hasNextPage)
|
||||
read_file: (result) => {
|
||||
return nextPageStr(result.hasNextPage)
|
||||
},
|
||||
list_dir: ([dirTreeStr, hasNextPage]) => {
|
||||
return dirTreeStr + nextPageStr(hasNextPage)
|
||||
list_dir: (result) => {
|
||||
const dirTreeStr = directoryResultToString(result)
|
||||
return dirTreeStr + nextPageStr(result.hasNextPage)
|
||||
},
|
||||
pathname_search: ([URIs, hasNextPage]) => {
|
||||
if (typeof URIs === 'string') return URIs
|
||||
return URIs.map(uri => uri.fsPath).join('\n') + nextPageStr(hasNextPage)
|
||||
pathname_search: (result) => {
|
||||
if (typeof result.uris === 'string') return result.uris
|
||||
return result.uris.map(uri => uri.fsPath).join('\n') + nextPageStr(result.hasNextPage)
|
||||
},
|
||||
search: ([URIs, hasNextPage]) => {
|
||||
if (typeof URIs === 'string') return URIs
|
||||
return URIs.map(uri => uri.fsPath).join('\n') + nextPageStr(hasNextPage)
|
||||
search: (result) => {
|
||||
if (typeof result.uris === 'string') return result.uris
|
||||
return result.uris.map(uri => uri.fsPath).join('\n') + nextPageStr(result.hasNextPage)
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -299,4 +362,3 @@ export class ToolsService implements IToolsService {
|
|||
}
|
||||
|
||||
registerSingleton(IToolsService, ToolsService, InstantiationType.Eager);
|
||||
|
||||
|
|
|
|||
109
src/vs/workbench/contrib/void/common/voidFileService.ts
Normal file
109
src/vs/workbench/contrib/void/common/voidFileService.ts
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { isWindows } from '../../../../base/common/platform.js';
|
||||
import { URI } from '../../../../base/common/uri.js';
|
||||
import { EndOfLinePreference } from '../../../../editor/common/model.js';
|
||||
import { IModelService } from '../../../../editor/common/services/model.js';
|
||||
import { IFileService } from '../../../../platform/files/common/files.js';
|
||||
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
|
||||
|
||||
// linebreak symbols
|
||||
export const allLinebreakSymbols = ['\r\n', '\n']
|
||||
export const _ln = isWindows ? allLinebreakSymbols[0] : allLinebreakSymbols[1]
|
||||
|
||||
export interface IVoidFileService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
readFile(uri: URI, range?: { startLineNumber: number, endLineNumber: number }): Promise<string>;
|
||||
readModel(uri: URI, range?: { startLineNumber: number, endLineNumber: number }): string | null;
|
||||
}
|
||||
|
||||
export const IVoidFileService = createDecorator<IVoidFileService>('VoidFileService');
|
||||
|
||||
// implemented by calling channel
|
||||
export class VoidFileService implements IVoidFileService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
constructor(
|
||||
@IModelService private readonly modelService: IModelService,
|
||||
@IFileService private readonly fileService: IFileService,
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
readFile = async (uri: URI, range?: { startLineNumber: number, endLineNumber: number }): Promise<string> => {
|
||||
|
||||
// attempt to read the model
|
||||
const modelResult = this.readModel(uri, range);
|
||||
if (modelResult) return modelResult;
|
||||
|
||||
// if no model, read the raw file
|
||||
const fileResult = await this._readFileRaw(uri, range);
|
||||
if (fileResult) return fileResult;
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
_readFileRaw = async (uri: URI, range?: { startLineNumber: number, endLineNumber: number }): Promise<string | null> => {
|
||||
|
||||
try { // this throws an error if no file exists (eg it was deleted)
|
||||
|
||||
const res = await this.fileService.readFile(uri);
|
||||
|
||||
if (range) {
|
||||
return res.value.toString()
|
||||
.split(_ln)
|
||||
.slice(range.startLineNumber - 1, range.endLineNumber)
|
||||
.join(_ln)
|
||||
}
|
||||
|
||||
return res.value.toString();
|
||||
|
||||
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
readModel = (uri: URI, range?: { startLineNumber: number, endLineNumber: number }): string | null => {
|
||||
|
||||
// read saved model (sometimes null if the user reloads application)
|
||||
let model = this.modelService.getModel(uri);
|
||||
|
||||
// check all opened models for the same `fsPath`
|
||||
if (!model) {
|
||||
const models = this.modelService.getModels();
|
||||
for (const m of models) {
|
||||
if (m.uri.fsPath === uri.fsPath) {
|
||||
model = m
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if still not found, return
|
||||
if (!model) { return null }
|
||||
|
||||
// if range, read it
|
||||
if (range) {
|
||||
return model.getValueInRange({
|
||||
startLineNumber: range.startLineNumber,
|
||||
endLineNumber: range.endLineNumber,
|
||||
startColumn: 1,
|
||||
endColumn: Number.MAX_VALUE
|
||||
}, EndOfLinePreference.LF);
|
||||
} else {
|
||||
return model.getValue(EndOfLinePreference.LF)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
registerSingleton(IVoidFileService, VoidFileService, InstantiationType.Eager);
|
||||
|
|
@ -11,7 +11,7 @@ import { registerSingleton, InstantiationType } from '../../../../platform/insta
|
|||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
|
||||
import { IMetricsService } from './metricsService.js';
|
||||
import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, VoidModelInfo, GlobalSettings, GlobalSettingName, defaultGlobalSettings, defaultProviderSettings, developerInfoOfModelName, modelInfoOfAutodetectedModelNames } from './voidSettingsTypes.js';
|
||||
import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, VoidModelInfo, GlobalSettings, GlobalSettingName, defaultGlobalSettings, defaultProviderSettings } from './voidSettingsTypes.js';
|
||||
|
||||
|
||||
const STORAGE_KEY = 'void.settingsServiceStorage'
|
||||
|
|
@ -32,8 +32,6 @@ type SetGlobalSettingFn = <T extends GlobalSettingName, >(settingName: T, newVal
|
|||
|
||||
export type ModelOption = { name: string, selection: ModelSelection }
|
||||
|
||||
|
||||
|
||||
export type VoidSettingsState = {
|
||||
readonly settingsOfProvider: SettingsOfProvider; // optionsOfProvider
|
||||
readonly modelSelectionOfFeature: ModelSelectionOfFeature; // stateOfFeature
|
||||
|
|
@ -65,7 +63,30 @@ export interface IVoidSettingsService {
|
|||
|
||||
|
||||
|
||||
const _updatedValidatedState = (state: Omit<VoidSettingsState, '_modelOptions'>) => {
|
||||
|
||||
const _updatedModelsAfterDefaultModelsChange = (defaultModelNames: string[], options: { existingModels: VoidModelInfo[] }) => {
|
||||
const { existingModels } = options
|
||||
|
||||
const existingModelsMap: Record<string, VoidModelInfo> = {}
|
||||
for (const existingModel of existingModels) {
|
||||
existingModelsMap[existingModel.modelName] = existingModel
|
||||
}
|
||||
|
||||
const newDefaultModels = defaultModelNames.map((modelName, i) => ({
|
||||
modelName,
|
||||
isDefault: true,
|
||||
isAutodetected: true,
|
||||
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
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
const _validatedState = (state: Omit<VoidSettingsState, '_modelOptions'>) => {
|
||||
|
||||
let newSettingsOfProvider = state.settingsOfProvider
|
||||
|
||||
|
|
@ -172,9 +193,6 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
|||
// A HACK BECAUSE WE ADDED DEEPSEEK (did not exist before, comes before readS)
|
||||
...{ deepseek: defaultSettingsOfProvider.deepseek },
|
||||
|
||||
// A HACK BECAUSE WE ADDED MISTRAL (did not exist before, comes before readS)
|
||||
...{ mistral: defaultSettingsOfProvider.mistral },
|
||||
|
||||
// A HACK BECAUSE WE ADDED XAI (did not exist before, comes before readS)
|
||||
...{ xAI: defaultSettingsOfProvider.xAI },
|
||||
|
||||
|
|
@ -206,7 +224,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
|||
modelSelectionOfFeature: newModelSelectionOfFeature,
|
||||
}
|
||||
|
||||
this.state = _updatedValidatedState(readS)
|
||||
this.state = _validatedState(readS)
|
||||
|
||||
resolver()
|
||||
this._onDidChangeState.fire()
|
||||
|
|
@ -253,7 +271,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
|||
globalSettings: newGlobalSettings,
|
||||
}
|
||||
|
||||
this.state = _updatedValidatedState(newState)
|
||||
this.state = _validatedState(newState)
|
||||
|
||||
await this._storeState()
|
||||
this._onDidChangeState.fire()
|
||||
|
|
@ -296,18 +314,13 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
|||
|
||||
|
||||
|
||||
|
||||
setAutodetectedModels(providerName: ProviderName, autodetectedModelNames: string[], logging: object) {
|
||||
|
||||
const { models } = this.state.settingsOfProvider[providerName]
|
||||
const oldModelNames = models.map(m => m.modelName)
|
||||
|
||||
|
||||
const newDefaultModels = modelInfoOfAutodetectedModelNames(autodetectedModelNames, { existingModels: models })
|
||||
const newModels = [
|
||||
...newDefaultModels, // swap out all the default models for the new default models
|
||||
...models.filter(m => !m.isDefault), // keep any non-default (custom) models
|
||||
]
|
||||
|
||||
const newModels = _updatedModelsAfterDefaultModelsChange(autodetectedModelNames, { existingModels: models })
|
||||
this.setSettingOfProvider(providerName, 'models', newModels)
|
||||
|
||||
// if the models changed, log it
|
||||
|
|
@ -341,7 +354,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
|||
if (existingIdx !== -1) return // if exists, do nothing
|
||||
const newModels = [
|
||||
...models,
|
||||
{ ...developerInfoOfModelName(modelName), modelName, isDefault: false, isHidden: false }
|
||||
{ modelName, isDefault: false, isHidden: false }
|
||||
]
|
||||
this.setSettingOfProvider(providerName, 'models', newModels)
|
||||
|
||||
|
|
|
|||
|
|
@ -7,364 +7,9 @@
|
|||
import { VoidSettingsState } from './voidSettingsService.js'
|
||||
|
||||
|
||||
|
||||
// developer info used in sendLLMMessage
|
||||
export type DeveloperInfoAtModel = {
|
||||
// USED:
|
||||
supportsSystemMessage: 'developer' | boolean, // if null, we will just do a string of system message. this is independent from separateSystemMessage, which takes priority and is passed directly in each provider's implementation.
|
||||
supportsTools: boolean, // we will just do a string of tool use if it doesn't support
|
||||
|
||||
// UNUSED (coming soon):
|
||||
// TODO!!! think tokens - deepseek
|
||||
_recognizedModelName: RecognizedModelName, // used to show user if model was auto-recognized
|
||||
_supportsStreaming: boolean, // we will just dump the final result if doesn't support it
|
||||
_supportsAutocompleteFIM: boolean, // we will just do a description of FIM if it doens't support <|fim_hole|>
|
||||
_maxTokens: number, // required
|
||||
}
|
||||
|
||||
export type DeveloperInfoAtProvider = {
|
||||
overrideSettingsForAllModels?: Partial<DeveloperInfoAtModel>; // any overrides for models that a provider might have (e.g. if a provider always supports tool use, even if we don't recognize the model we can set tools to true)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export type VoidModelInfo = { // <-- STATEFUL
|
||||
modelName: string,
|
||||
isDefault: boolean, // whether or not it's a default for its provider
|
||||
isHidden: boolean, // whether or not the user is hiding it (switched off)
|
||||
isAutodetected?: boolean, // whether the model was autodetected by polling
|
||||
} & DeveloperInfoAtModel
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export const recognizedModels = [
|
||||
// chat
|
||||
'OpenAI 4o',
|
||||
'Anthropic Claude',
|
||||
'Llama 3.x',
|
||||
'Deepseek Chat', // deepseek coder v2 is now merged into chat (V3) https://api-docs.deepseek.com/updates#deepseek-coder--deepseek-chat-upgraded-to-deepseek-v25-model
|
||||
'xAI Grok',
|
||||
// 'xAI Grok',
|
||||
// 'Google Gemini, Gemma',
|
||||
// 'Microsoft Phi4',
|
||||
|
||||
|
||||
// coding (autocomplete)
|
||||
'Alibaba Qwen2.5 Coder Instruct', // we recommend this over Qwen2.5
|
||||
'Mistral Codestral',
|
||||
|
||||
// thinking
|
||||
'OpenAI o1',
|
||||
'Deepseek R1',
|
||||
|
||||
// general
|
||||
// 'Mixtral 8x7b'
|
||||
// 'Qwen2.5',
|
||||
|
||||
] as const
|
||||
|
||||
type RecognizedModelName = (typeof recognizedModels)[number] | '<GENERAL>'
|
||||
|
||||
|
||||
export function recognizedModelOfModelName(modelName: string): RecognizedModelName {
|
||||
const lower = modelName.toLowerCase();
|
||||
|
||||
if (lower.includes('gpt-4o'))
|
||||
return 'OpenAI 4o';
|
||||
if (lower.includes('claude'))
|
||||
return 'Anthropic Claude';
|
||||
if (lower.includes('llama'))
|
||||
return 'Llama 3.x';
|
||||
if (lower.includes('qwen2.5-coder'))
|
||||
return 'Alibaba Qwen2.5 Coder Instruct';
|
||||
if (lower.includes('mistral'))
|
||||
return 'Mistral Codestral';
|
||||
if (/\bo1\b/.test(lower) || /\bo3\b/.test(lower)) // o1, o3
|
||||
return 'OpenAI o1';
|
||||
if (lower.includes('deepseek-r1') || lower.includes('deepseek-reasoner'))
|
||||
return 'Deepseek R1';
|
||||
if (lower.includes('deepseek'))
|
||||
return 'Deepseek Chat'
|
||||
if (lower.includes('grok'))
|
||||
return 'xAI Grok'
|
||||
|
||||
return '<GENERAL>';
|
||||
}
|
||||
|
||||
|
||||
const developerInfoAtProvider: { [providerName in ProviderName]: DeveloperInfoAtProvider } = {
|
||||
'anthropic': {
|
||||
overrideSettingsForAllModels: {
|
||||
supportsSystemMessage: true,
|
||||
supportsTools: true,
|
||||
_supportsAutocompleteFIM: false,
|
||||
_supportsStreaming: true,
|
||||
}
|
||||
},
|
||||
'deepseek': {
|
||||
overrideSettingsForAllModels: {
|
||||
}
|
||||
},
|
||||
'ollama': {
|
||||
},
|
||||
'openRouter': {
|
||||
},
|
||||
'openAICompatible': {
|
||||
},
|
||||
'openAI': {
|
||||
},
|
||||
'gemini': {
|
||||
},
|
||||
'mistral': {
|
||||
},
|
||||
'groq': {
|
||||
},
|
||||
'xAI': {
|
||||
},
|
||||
'vLLM': {
|
||||
},
|
||||
}
|
||||
export const developerInfoOfProviderName = (providerName: ProviderName): Partial<DeveloperInfoAtProvider> => {
|
||||
return developerInfoAtProvider[providerName] ?? {}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// providerName is optional, but gives some extra fallbacks if provided
|
||||
const developerInfoOfRecognizedModelName: { [recognizedModel in RecognizedModelName]: Omit<DeveloperInfoAtModel, '_recognizedModelName'> } = {
|
||||
'OpenAI 4o': {
|
||||
supportsSystemMessage: true,
|
||||
supportsTools: true,
|
||||
_supportsAutocompleteFIM: false,
|
||||
_supportsStreaming: true,
|
||||
_maxTokens: 4096,
|
||||
},
|
||||
|
||||
'Anthropic Claude': {
|
||||
supportsSystemMessage: true,
|
||||
supportsTools: false,
|
||||
_supportsAutocompleteFIM: false,
|
||||
_supportsStreaming: false,
|
||||
_maxTokens: 4096,
|
||||
},
|
||||
|
||||
'Llama 3.x': {
|
||||
supportsSystemMessage: true,
|
||||
supportsTools: true,
|
||||
_supportsAutocompleteFIM: false,
|
||||
_supportsStreaming: false,
|
||||
_maxTokens: 4096,
|
||||
},
|
||||
|
||||
'xAI Grok': {
|
||||
supportsSystemMessage: true,
|
||||
supportsTools: true,
|
||||
_supportsAutocompleteFIM: false,
|
||||
_supportsStreaming: true,
|
||||
_maxTokens: 4096,
|
||||
|
||||
},
|
||||
|
||||
'Deepseek Chat': {
|
||||
supportsSystemMessage: true,
|
||||
supportsTools: false,
|
||||
_supportsAutocompleteFIM: false,
|
||||
_supportsStreaming: false,
|
||||
_maxTokens: 4096,
|
||||
},
|
||||
|
||||
'Alibaba Qwen2.5 Coder Instruct': {
|
||||
supportsSystemMessage: true,
|
||||
supportsTools: true,
|
||||
_supportsAutocompleteFIM: false,
|
||||
_supportsStreaming: false,
|
||||
_maxTokens: 4096,
|
||||
},
|
||||
|
||||
'Mistral Codestral': {
|
||||
supportsSystemMessage: true,
|
||||
supportsTools: true,
|
||||
_supportsAutocompleteFIM: false,
|
||||
_supportsStreaming: false,
|
||||
_maxTokens: 4096,
|
||||
},
|
||||
|
||||
'OpenAI o1': {
|
||||
supportsSystemMessage: 'developer',
|
||||
supportsTools: false,
|
||||
_supportsAutocompleteFIM: false,
|
||||
_supportsStreaming: true,
|
||||
_maxTokens: 4096,
|
||||
},
|
||||
|
||||
'Deepseek R1': {
|
||||
supportsSystemMessage: false,
|
||||
supportsTools: false,
|
||||
_supportsAutocompleteFIM: false,
|
||||
_supportsStreaming: false,
|
||||
_maxTokens: 4096,
|
||||
},
|
||||
|
||||
|
||||
'<GENERAL>': {
|
||||
supportsSystemMessage: false,
|
||||
supportsTools: false,
|
||||
_supportsAutocompleteFIM: false,
|
||||
_supportsStreaming: false,
|
||||
_maxTokens: 4096,
|
||||
},
|
||||
}
|
||||
export const developerInfoOfModelName = (modelName: string, overrides?: Partial<DeveloperInfoAtModel>): DeveloperInfoAtModel => {
|
||||
const recognizedModelName = recognizedModelOfModelName(modelName)
|
||||
return {
|
||||
_recognizedModelName: recognizedModelName,
|
||||
...developerInfoOfRecognizedModelName[recognizedModelName],
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// creates `modelInfo` from `modelNames`
|
||||
export const modelInfoOfDefaultModelNames = (defaultModelNames: string[]): VoidModelInfo[] => {
|
||||
return defaultModelNames.map((modelName, i) => ({
|
||||
modelName,
|
||||
isDefault: true,
|
||||
isAutodetected: false,
|
||||
isHidden: defaultModelNames.length >= 10, // hide all models if there are a ton of them, and make user enable them individually
|
||||
...developerInfoOfModelName(modelName),
|
||||
}))
|
||||
}
|
||||
|
||||
export const modelInfoOfAutodetectedModelNames = (defaultModelNames: string[], options: { existingModels: VoidModelInfo[] }) => {
|
||||
const { existingModels } = options
|
||||
|
||||
const existingModelsMap: Record<string, VoidModelInfo> = {}
|
||||
for (const existingModel of existingModels) {
|
||||
existingModelsMap[existingModel.modelName] = existingModel
|
||||
}
|
||||
|
||||
return defaultModelNames.map((modelName, i) => ({
|
||||
modelName,
|
||||
isDefault: true,
|
||||
isAutodetected: true,
|
||||
isHidden: !!existingModelsMap[modelName]?.isHidden,
|
||||
...developerInfoOfModelName(modelName)
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// https://docs.anthropic.com/en/docs/about-claude/models
|
||||
export const defaultAnthropicModels = modelInfoOfDefaultModelNames([
|
||||
'claude-3-5-sonnet-20241022',
|
||||
'claude-3-5-haiku-20241022',
|
||||
'claude-3-opus-20240229',
|
||||
'claude-3-sonnet-20240229',
|
||||
// 'claude-3-haiku-20240307',
|
||||
])
|
||||
|
||||
|
||||
// https://platform.openai.com/docs/models/gp
|
||||
export const defaultOpenAIModels = modelInfoOfDefaultModelNames([
|
||||
'o1',
|
||||
'o1-mini',
|
||||
'o3-mini',
|
||||
'gpt-4o',
|
||||
'gpt-4o-mini',
|
||||
// 'gpt-4o-2024-05-13',
|
||||
// 'gpt-4o-2024-08-06',
|
||||
// 'gpt-4o-mini-2024-07-18',
|
||||
// 'gpt-4-turbo',
|
||||
// 'gpt-4-turbo-2024-04-09',
|
||||
// 'gpt-4-turbo-preview',
|
||||
// 'gpt-4-0125-preview',
|
||||
// 'gpt-4-1106-preview',
|
||||
// 'gpt-4',
|
||||
// 'gpt-4-0613',
|
||||
// 'gpt-3.5-turbo-0125',
|
||||
// 'gpt-3.5-turbo',
|
||||
// 'gpt-3.5-turbo-1106',
|
||||
])
|
||||
|
||||
// https://platform.openai.com/docs/models/gp
|
||||
export const defaultDeepseekModels = modelInfoOfDefaultModelNames([
|
||||
'deepseek-chat',
|
||||
'deepseek-reasoner',
|
||||
])
|
||||
|
||||
|
||||
// https://console.groq.com/docs/models
|
||||
export const defaultGroqModels = modelInfoOfDefaultModelNames([
|
||||
"llama3-70b-8192",
|
||||
"llama-3.3-70b-versatile",
|
||||
"llama-3.1-8b-instant",
|
||||
"gemma2-9b-it",
|
||||
"mixtral-8x7b-32768"
|
||||
])
|
||||
|
||||
|
||||
export const defaultGeminiModels = modelInfoOfDefaultModelNames([
|
||||
'gemini-1.5-flash',
|
||||
'gemini-1.5-pro',
|
||||
'gemini-1.5-flash-8b',
|
||||
'gemini-2.0-flash-exp',
|
||||
'gemini-2.0-flash-thinking-exp-1219',
|
||||
'learnlm-1.5-pro-experimental'
|
||||
])
|
||||
|
||||
export const defaultMistralModels = modelInfoOfDefaultModelNames([
|
||||
"codestral-latest",
|
||||
"open-codestral-mamba",
|
||||
"open-mistral-nemo",
|
||||
"mistral-large-latest",
|
||||
"pixtral-large-latest",
|
||||
"ministral-3b-latest",
|
||||
"ministral-8b-latest",
|
||||
"mistral-small-latest",
|
||||
])
|
||||
|
||||
export const defaultXAIModels = modelInfoOfDefaultModelNames([
|
||||
'grok-2-latest',
|
||||
'grok-3-latest',
|
||||
])
|
||||
// export const parseMaxTokensStr = (maxTokensStr: string) => {
|
||||
// // parse the string but only if the full string is a valid number, eg parseInt('100abc') should return NaN
|
||||
// const int = isNaN(Number(maxTokensStr)) ? undefined : parseInt(maxTokensStr)
|
||||
// if (Number.isNaN(int))
|
||||
// return undefined
|
||||
// return int
|
||||
// }
|
||||
|
||||
|
||||
|
||||
|
||||
export const anthropicMaxPossibleTokens = (modelName: string) => {
|
||||
if (modelName === 'claude-3-5-sonnet-20241022'
|
||||
|| modelName === 'claude-3-5-haiku-20241022')
|
||||
return 8192
|
||||
if (modelName === 'claude-3-opus-20240229'
|
||||
|| modelName === 'claude-3-sonnet-20240229'
|
||||
|| modelName === 'claude-3-haiku-20240307')
|
||||
return 4096
|
||||
return 1024 // return a reasonably small number if they're using a different model
|
||||
}
|
||||
|
||||
|
||||
type UnionOfKeys<T> = T extends T ? keyof T : never;
|
||||
|
||||
|
||||
|
||||
export const defaultProviderSettings = {
|
||||
anthropic: {
|
||||
apiKey: '',
|
||||
|
|
@ -394,14 +39,70 @@ export const defaultProviderSettings = {
|
|||
groq: {
|
||||
apiKey: '',
|
||||
},
|
||||
mistral: {
|
||||
apiKey: ''
|
||||
},
|
||||
xAI: {
|
||||
apiKey: ''
|
||||
},
|
||||
} as const
|
||||
|
||||
|
||||
|
||||
|
||||
export const defaultModelsOfProvider = {
|
||||
openAI: [ // https://platform.openai.com/docs/models/gp
|
||||
'o1',
|
||||
'o3-mini',
|
||||
'o1-mini',
|
||||
'gpt-4o',
|
||||
'gpt-4o-mini',
|
||||
],
|
||||
anthropic: [ // https://docs.anthropic.com/en/docs/about-claude/models
|
||||
'claude-3-5-sonnet-latest',
|
||||
'claude-3-5-haiku-latest',
|
||||
'claude-3-opus-latest',
|
||||
],
|
||||
xAI: [ // https://docs.x.ai/docs/models?cluster=us-east-1
|
||||
'grok-2-latest',
|
||||
'grok-3-latest',
|
||||
],
|
||||
gemini: [ // https://ai.google.dev/gemini-api/docs/models/gemini
|
||||
'gemini-2.0-flash',
|
||||
'gemini-1.5-flash',
|
||||
'gemini-1.5-pro',
|
||||
'gemini-1.5-flash-8b',
|
||||
'gemini-2.0-flash-thinking-exp',
|
||||
],
|
||||
deepseek: [ // https://api-docs.deepseek.com/quick_start/pricing
|
||||
'deepseek-chat',
|
||||
'deepseek-reasoner',
|
||||
],
|
||||
ollama: [ // autodetected
|
||||
],
|
||||
vLLM: [ // autodetected
|
||||
],
|
||||
openRouter: [ // https://openrouter.ai/models
|
||||
'anthropic/claude-3.5-sonnet',
|
||||
'deepseek/deepseek-r1',
|
||||
'mistralai/codestral-2501',
|
||||
'qwen/qwen-2.5-coder-32b-instruct',
|
||||
],
|
||||
groq: [ // https://console.groq.com/docs/models
|
||||
'llama-3.3-70b-versatile',
|
||||
'llama-3.1-8b-instant',
|
||||
'qwen-2.5-coder-32b', // preview mode (experimental)
|
||||
],
|
||||
// not supporting mistral right now- it's last on Void usage, and a huge pain to set up since it's nonstandard (it supports codestral FIM but it's on v1/fim/completions, etc)
|
||||
// mistral: [ // https://docs.mistral.ai/getting-started/models/models_overview/
|
||||
// 'codestral-latest',
|
||||
// 'mistral-large-latest',
|
||||
// 'ministral-3b-latest',
|
||||
// 'ministral-8b-latest',
|
||||
// ],
|
||||
openAICompatible: [], // fallback
|
||||
} as const satisfies Record<ProviderName, string[]>
|
||||
|
||||
|
||||
|
||||
|
||||
export type ProviderName = keyof typeof defaultProviderSettings
|
||||
export const providerNames = Object.keys(defaultProviderSettings) as ProviderName[]
|
||||
|
||||
|
|
@ -418,6 +119,14 @@ export const customSettingNamesOfProvider = (providerName: ProviderName) => {
|
|||
|
||||
|
||||
|
||||
export type VoidModelInfo = { // <-- STATEFUL
|
||||
modelName: string,
|
||||
isDefault: boolean, // whether or not it's a default for its provider
|
||||
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
|
||||
|
||||
|
||||
|
||||
type CommonProviderSettings = {
|
||||
_didFillInProviderSettings: boolean | undefined, // undefined initially, computed when user types in all fields
|
||||
|
|
@ -434,10 +143,6 @@ export type SettingsOfProvider = {
|
|||
|
||||
export type SettingName = keyof SettingsAtProvider<ProviderName>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
type DisplayInfoForProviderName = {
|
||||
title: string,
|
||||
desc?: string,
|
||||
|
|
@ -489,11 +194,6 @@ export const displayInfoOfProviderName = (providerName: ProviderName): DisplayIn
|
|||
title: 'Groq.com API',
|
||||
}
|
||||
}
|
||||
else if (providerName === 'mistral') {
|
||||
return {
|
||||
title: 'Mistral API',
|
||||
}
|
||||
}
|
||||
else if (providerName === 'xAI') {
|
||||
return {
|
||||
title: 'xAI API',
|
||||
|
|
@ -505,9 +205,10 @@ export const displayInfoOfProviderName = (providerName: ProviderName): DisplayIn
|
|||
}
|
||||
|
||||
type DisplayInfo = {
|
||||
title: string,
|
||||
placeholder: string,
|
||||
subTextMd?: string,
|
||||
title: string;
|
||||
placeholder: string;
|
||||
subTextMd?: string;
|
||||
isPasswordField?: boolean;
|
||||
}
|
||||
export const displayInfoOfSettingName = (providerName: ProviderName, settingName: SettingName): DisplayInfo => {
|
||||
if (settingName === 'apiKey') {
|
||||
|
|
@ -522,10 +223,9 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
|
|||
providerName === 'openRouter' ? 'sk-or-key...' : // sk-or-v1-key
|
||||
providerName === 'gemini' ? 'key...' :
|
||||
providerName === 'groq' ? 'gsk_key...' :
|
||||
providerName === 'mistral' ? 'key...' :
|
||||
providerName === 'openAICompatible' ? 'sk-key...' :
|
||||
providerName === 'xAI' ? 'xai-key...' :
|
||||
'',
|
||||
providerName === 'openAICompatible' ? 'sk-key...' :
|
||||
providerName === 'xAI' ? 'xai-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).' :
|
||||
|
|
@ -533,17 +233,17 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
|
|||
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 === 'mistral' ? 'Get your [API Key here](https://console.mistral.ai/api-keys/).' :
|
||||
providerName === 'xAI' ? 'Get your [API Key here](https://console.x.ai).' :
|
||||
providerName === 'openAICompatible' ? undefined :
|
||||
'',
|
||||
providerName === 'xAI' ? 'Get your [API Key here](https://console.x.ai).' :
|
||||
providerName === 'openAICompatible' ? undefined :
|
||||
'',
|
||||
isPasswordField: true,
|
||||
}
|
||||
}
|
||||
else if (settingName === 'endpoint') {
|
||||
return {
|
||||
title: providerName === 'ollama' ? 'Endpoint' :
|
||||
providerName === 'vLLM' ? 'Endpoint' :
|
||||
providerName === 'openAICompatible' ? 'baseURL' :// (do not include /chat/completions)
|
||||
providerName === 'openAICompatible' ? 'baseURL' : // (do not include /chat/completions)
|
||||
'(never)',
|
||||
|
||||
placeholder: providerName === 'ollama' ? defaultProviderSettings.ollama.endpoint
|
||||
|
|
@ -582,110 +282,77 @@ const defaultCustomSettings: Record<CustomSettingName, undefined> = {
|
|||
}
|
||||
|
||||
|
||||
|
||||
export const voidInitModelOptions = {
|
||||
anthropic: {
|
||||
models: defaultAnthropicModels,
|
||||
},
|
||||
openAI: {
|
||||
models: defaultOpenAIModels,
|
||||
},
|
||||
deepseek: {
|
||||
models: defaultDeepseekModels,
|
||||
},
|
||||
ollama: {
|
||||
models: [],
|
||||
},
|
||||
vLLM: {
|
||||
models: [],
|
||||
},
|
||||
openRouter: {
|
||||
models: [], // any string
|
||||
},
|
||||
openAICompatible: {
|
||||
models: [],
|
||||
},
|
||||
gemini: {
|
||||
models: defaultGeminiModels,
|
||||
},
|
||||
groq: {
|
||||
models: defaultGroqModels,
|
||||
},
|
||||
mistral: {
|
||||
models: defaultMistralModels,
|
||||
},
|
||||
xAI: {
|
||||
models: defaultXAIModels,
|
||||
const modelInfoOfDefaultModelNames = (defaultModelNames: string[]): { models: VoidModelInfo[] } => {
|
||||
return {
|
||||
models: defaultModelNames.map((modelName, i) => ({
|
||||
modelName,
|
||||
isDefault: true,
|
||||
isAutodetected: false,
|
||||
isHidden: defaultModelNames.length >= 10, // hide all models if there are a ton of them, and make user enable them individually
|
||||
}))
|
||||
}
|
||||
} satisfies Record<ProviderName, any>
|
||||
|
||||
}
|
||||
|
||||
// used when waiting and for a type reference
|
||||
export const defaultSettingsOfProvider: SettingsOfProvider = {
|
||||
anthropic: {
|
||||
...defaultCustomSettings,
|
||||
...defaultProviderSettings.anthropic,
|
||||
...voidInitModelOptions.anthropic,
|
||||
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.anthropic),
|
||||
_didFillInProviderSettings: undefined,
|
||||
},
|
||||
openAI: {
|
||||
...defaultCustomSettings,
|
||||
...defaultProviderSettings.openAI,
|
||||
...voidInitModelOptions.openAI,
|
||||
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.openAI),
|
||||
_didFillInProviderSettings: undefined,
|
||||
},
|
||||
deepseek: {
|
||||
...defaultCustomSettings,
|
||||
...defaultProviderSettings.deepseek,
|
||||
...voidInitModelOptions.deepseek,
|
||||
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.deepseek),
|
||||
_didFillInProviderSettings: undefined,
|
||||
},
|
||||
gemini: {
|
||||
...defaultCustomSettings,
|
||||
...defaultProviderSettings.gemini,
|
||||
...voidInitModelOptions.gemini,
|
||||
_didFillInProviderSettings: undefined,
|
||||
},
|
||||
mistral: {
|
||||
...defaultCustomSettings,
|
||||
...defaultProviderSettings.mistral,
|
||||
...voidInitModelOptions.mistral,
|
||||
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.gemini),
|
||||
_didFillInProviderSettings: undefined,
|
||||
},
|
||||
xAI: {
|
||||
...defaultCustomSettings,
|
||||
...defaultProviderSettings.xAI,
|
||||
...voidInitModelOptions.xAI,
|
||||
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.xAI),
|
||||
_didFillInProviderSettings: undefined,
|
||||
},
|
||||
groq: { // aggregator
|
||||
...defaultCustomSettings,
|
||||
...defaultProviderSettings.groq,
|
||||
...voidInitModelOptions.groq,
|
||||
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.groq),
|
||||
_didFillInProviderSettings: undefined,
|
||||
},
|
||||
openRouter: { // aggregator
|
||||
...defaultCustomSettings,
|
||||
...defaultProviderSettings.openRouter,
|
||||
...voidInitModelOptions.openRouter,
|
||||
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.openRouter),
|
||||
_didFillInProviderSettings: undefined,
|
||||
},
|
||||
openAICompatible: { // aggregator
|
||||
...defaultCustomSettings,
|
||||
...defaultProviderSettings.openAICompatible,
|
||||
...voidInitModelOptions.openAICompatible,
|
||||
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.openAICompatible),
|
||||
_didFillInProviderSettings: undefined,
|
||||
},
|
||||
ollama: { // aggregator
|
||||
...defaultCustomSettings,
|
||||
...defaultProviderSettings.ollama,
|
||||
...voidInitModelOptions.ollama,
|
||||
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.ollama),
|
||||
_didFillInProviderSettings: undefined,
|
||||
},
|
||||
vLLM: { // aggregator
|
||||
...defaultCustomSettings,
|
||||
...defaultProviderSettings.vLLM,
|
||||
...voidInitModelOptions.vLLM,
|
||||
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.vLLM),
|
||||
_didFillInProviderSettings: undefined,
|
||||
},
|
||||
}
|
||||
|
|
|
|||
1000
src/vs/workbench/contrib/void/electron-main/llmMessage/MODELS.ts
Normal file
1000
src/vs/workbench/contrib/void/electron-main/llmMessage/MODELS.ts
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,96 +0,0 @@
|
|||
// /*--------------------------------------------------------------------------------------
|
||||
// * Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
// * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
// *--------------------------------------------------------------------------------------*/
|
||||
|
||||
// import Groq from 'groq-sdk';
|
||||
// import { _InternalSendLLMChatMessageFnType } from '../../common/llmMessageTypes.js';
|
||||
|
||||
// // Groq
|
||||
// export const sendGroqChat: _InternalSendLLMChatMessageFnType = async ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => {
|
||||
// let fullText = '';
|
||||
|
||||
// const thisConfig = settingsOfProvider.groq
|
||||
|
||||
// const groq = new Groq({
|
||||
// apiKey: thisConfig.apiKey,
|
||||
// dangerouslyAllowBrowser: true
|
||||
// });
|
||||
|
||||
// await groq.chat.completions
|
||||
// .create({
|
||||
// messages: messages,
|
||||
// model: modelName,
|
||||
// stream: true,
|
||||
// })
|
||||
// .then(async response => {
|
||||
// _setAborter(() => response.controller.abort())
|
||||
// // when receive text
|
||||
// for await (const chunk of response) {
|
||||
// const newText = chunk.choices[0]?.delta?.content || '';
|
||||
// fullText += newText;
|
||||
// onText({ newText, fullText });
|
||||
// }
|
||||
|
||||
// onFinalMessage({ fullText, tools: [] });
|
||||
// })
|
||||
// .catch(error => {
|
||||
// onError({ message: error + '', fullError: error });
|
||||
// })
|
||||
|
||||
|
||||
// };
|
||||
|
||||
|
||||
|
||||
// /*--------------------------------------------------------------------------------------
|
||||
// * Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
// * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
// *--------------------------------------------------------------------------------------*/
|
||||
|
||||
// import { Mistral } from '@mistralai/mistralai';
|
||||
// import { _InternalSendLLMChatMessageFnType } from '../../common/llmMessageTypes.js';
|
||||
|
||||
// // Mistral
|
||||
// export const sendMistralChat: _InternalSendLLMChatMessageFnType = async ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => {
|
||||
// let fullText = '';
|
||||
|
||||
// const thisConfig = settingsOfProvider.mistral;
|
||||
|
||||
// const mistral = new Mistral({
|
||||
// apiKey: thisConfig.apiKey,
|
||||
// })
|
||||
|
||||
// await mistral.chat
|
||||
// .stream({
|
||||
// messages: messages,
|
||||
// model: modelName,
|
||||
// stream: true,
|
||||
// })
|
||||
// .then(async response => {
|
||||
// // Mistral has a really nonstandard API - no interrupt and weird stream types
|
||||
// _setAborter(() => { console.log('Mistral does not support interrupts! Further messages will just be ignored.') });
|
||||
// // when receive text
|
||||
// for await (const chunk of response) {
|
||||
// const c = chunk.data.choices[0].delta.content || ''
|
||||
// const newText = (
|
||||
// typeof c === 'string' ? c
|
||||
// : c?.map(c => c.type === 'text' ? c.text : c.type).join('\n')
|
||||
// )
|
||||
// fullText += newText;
|
||||
// onText({ newText, fullText });
|
||||
// }
|
||||
|
||||
// onFinalMessage({ fullText, tools: [] });
|
||||
// })
|
||||
// .catch(error => {
|
||||
// onError({ message: error + '', fullError: error });
|
||||
// })
|
||||
// }
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -1,114 +0,0 @@
|
|||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import Anthropic from '@anthropic-ai/sdk';
|
||||
import { _InternalSendLLMChatMessageFnType } from '../../common/llmMessageTypes.js';
|
||||
import { anthropicMaxPossibleTokens, developerInfoOfModelName, developerInfoOfProviderName } from '../../common/voidSettingsTypes.js';
|
||||
import { InternalToolInfo } from '../../common/toolsService.js';
|
||||
import { addSystemMessageAndToolSupport } from './preprocessLLMMessages.js';
|
||||
import { isAToolName } from './postprocessToolCalls.js';
|
||||
|
||||
|
||||
|
||||
|
||||
export const toAnthropicTool = (toolInfo: InternalToolInfo) => {
|
||||
const { name, description, params, required } = toolInfo
|
||||
return {
|
||||
name: name,
|
||||
description: description,
|
||||
input_schema: {
|
||||
type: 'object',
|
||||
properties: params,
|
||||
required: required,
|
||||
}
|
||||
} satisfies Anthropic.Messages.Tool
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export const sendAnthropicChat: _InternalSendLLMChatMessageFnType = ({ messages: messages_, providerName, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, aiInstructions, tools: tools_ }) => {
|
||||
|
||||
const thisConfig = settingsOfProvider.anthropic
|
||||
|
||||
const maxTokens = anthropicMaxPossibleTokens(modelName)
|
||||
if (maxTokens === undefined) {
|
||||
onError({ message: `Please set a value for Max Tokens.`, fullError: null })
|
||||
return
|
||||
}
|
||||
|
||||
const { messages, separateSystemMessageStr } = addSystemMessageAndToolSupport(modelName, providerName, messages_, aiInstructions, { separateSystemMessage: true })
|
||||
|
||||
const { overrideSettingsForAllModels } = developerInfoOfProviderName(providerName)
|
||||
const { supportsTools } = developerInfoOfModelName(modelName, overrideSettingsForAllModels)
|
||||
|
||||
const anthropic = new Anthropic({ apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true });
|
||||
|
||||
const tools = (supportsTools && ((tools_?.length ?? 0) !== 0)) ? tools_?.map(tool => toAnthropicTool(tool)) : undefined
|
||||
|
||||
const stream = anthropic.messages.stream({
|
||||
system: separateSystemMessageStr,
|
||||
messages: messages,
|
||||
model: modelName,
|
||||
max_tokens: maxTokens,
|
||||
tools: tools,
|
||||
tool_choice: tools ? { type: 'auto', disable_parallel_tool_use: true } : undefined // one tool use at a time
|
||||
})
|
||||
|
||||
|
||||
// when receive text
|
||||
stream.on('text', (newText, fullText) => {
|
||||
onText({ newText, fullText })
|
||||
})
|
||||
|
||||
|
||||
// // can do tool use streaming
|
||||
// const toolCallOfIndex: { [index: string]: { name: string, args: string } } = {}
|
||||
// stream.on('streamEvent', e => {
|
||||
// if (e.type === 'content_block_start') {
|
||||
// if (e.content_block.type !== 'tool_use') return
|
||||
// const index = e.index
|
||||
// if (!toolCallOfIndex[index]) toolCallOfIndex[index] = { name: '', args: '' }
|
||||
// toolCallOfIndex[index].name += e.content_block.name ?? ''
|
||||
// toolCallOfIndex[index].args += e.content_block.input ?? ''
|
||||
// }
|
||||
// else if (e.type === 'content_block_delta') {
|
||||
// if (e.delta.type !== 'input_json_delta') return
|
||||
// toolCallOfIndex[e.index].args += e.delta.partial_json
|
||||
// }
|
||||
// // TODO!!!!!
|
||||
// // onText({})
|
||||
// })
|
||||
|
||||
// when we get the final message on this stream (or when error/fail)
|
||||
stream.on('finalMessage', (response) => {
|
||||
// stringify the response's content
|
||||
const content = response.content.map(c => c.type === 'text' ? c.text : '').join('\n\n')
|
||||
const toolCalls = response.content
|
||||
.map(c => {
|
||||
if (c.type !== 'tool_use') return null
|
||||
if (!isAToolName(c.name)) return null
|
||||
return c.type === 'tool_use' ? { name: c.name, params: JSON.stringify(c.input), id: c.id } : null
|
||||
})
|
||||
.filter(t => !!t)
|
||||
|
||||
onFinalMessage({ fullText: content, toolCalls })
|
||||
})
|
||||
|
||||
stream.on('error', (error) => {
|
||||
// the most common error will be invalid API key (401), so we handle this with a nice message
|
||||
if (error instanceof Anthropic.APIError && error.status === 401) {
|
||||
onError({ message: 'Invalid API key.', fullError: error })
|
||||
}
|
||||
else {
|
||||
onError({ message: error + '', fullError: error }) // anthropic errors can be stringified nicely like this
|
||||
}
|
||||
})
|
||||
|
||||
// TODO need to test this to make sure it works, it might throw an error
|
||||
_setAborter(() => stream.controller.abort())
|
||||
|
||||
};
|
||||
|
|
@ -1,124 +0,0 @@
|
|||
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Ollama } from 'ollama';
|
||||
import { _InternalModelListFnType, _InternalSendLLMFIMMessageFnType, _InternalSendLLMChatMessageFnType, OllamaModelResponse } from '../../common/llmMessageTypes.js';
|
||||
import { defaultProviderSettings } from '../../common/voidSettingsTypes.js';
|
||||
|
||||
export const ollamaList: _InternalModelListFnType<OllamaModelResponse> = async ({ onSuccess: onSuccess_, onError: onError_, settingsOfProvider }) => {
|
||||
|
||||
const onSuccess = ({ models }: { models: OllamaModelResponse[] }) => {
|
||||
onSuccess_({ models })
|
||||
}
|
||||
|
||||
const onError = ({ error }: { error: string }) => {
|
||||
onError_({ error })
|
||||
}
|
||||
|
||||
try {
|
||||
const thisConfig = settingsOfProvider.ollama
|
||||
// if endpoint is empty, normally ollama will send to 11434, but we want it to fail - the user should type it in
|
||||
if (!thisConfig.endpoint) throw new Error(`Ollama Endpoint was empty (please enter ${defaultProviderSettings.ollama.endpoint} in Void if you want the default url).`)
|
||||
|
||||
const ollama = new Ollama({ host: thisConfig.endpoint })
|
||||
ollama.list()
|
||||
.then((response) => {
|
||||
const { models } = response
|
||||
onSuccess({ models })
|
||||
})
|
||||
.catch((error) => {
|
||||
onError({ error: error + '' })
|
||||
})
|
||||
}
|
||||
catch (error) {
|
||||
onError({ error: error + '' })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// export const sendOllamaFIM: _InternalSendLLMFIMMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => {
|
||||
|
||||
// const thisConfig = settingsOfProvider.ollama
|
||||
// // if endpoint is empty, normally ollama will send to 11434, but we want it to fail - the user should type it in
|
||||
// if (!thisConfig.endpoint) throw new Error(`Ollama Endpoint was empty (please enter ${defaultProviderSettings.ollama.endpoint} if you want the default).`)
|
||||
|
||||
// let fullText = ''
|
||||
|
||||
// const ollama = new Ollama({ host: thisConfig.endpoint })
|
||||
|
||||
// ollama.generate({
|
||||
// model: modelName,
|
||||
// prompt: messages.prefix,
|
||||
// suffix: messages.suffix,
|
||||
// options: {
|
||||
// stop: messages.stopTokens,
|
||||
// num_predict: 300, // max tokens
|
||||
// // repeat_penalty: 1,
|
||||
// },
|
||||
// raw: true,
|
||||
// stream: true,
|
||||
// })
|
||||
// .then(async stream => {
|
||||
// _setAborter(() => stream.abort())
|
||||
// // iterate through the stream
|
||||
// for await (const chunk of stream) {
|
||||
// const newText = chunk.response;
|
||||
// fullText += newText;
|
||||
// onText({ newText, fullText });
|
||||
// }
|
||||
// onFinalMessage({ fullText, tools: [] });
|
||||
// })
|
||||
// // when error/fail
|
||||
// .catch((error) => {
|
||||
// onError({ message: error + '', fullError: error })
|
||||
// })
|
||||
// };
|
||||
|
||||
|
||||
// // Ollama
|
||||
// export const sendOllamaChat: _InternalSendLLMChatMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => {
|
||||
|
||||
// const thisConfig = settingsOfProvider.ollama
|
||||
// // if endpoint is empty, normally ollama will send to 11434, but we want it to fail - the user should type it in
|
||||
// if (!thisConfig.endpoint) throw new Error(`Ollama Endpoint was empty (please enter ${defaultProviderSettings.ollama.endpoint} if you want the default).`)
|
||||
|
||||
// let fullText = ''
|
||||
|
||||
// const ollama = new Ollama({ host: thisConfig.endpoint })
|
||||
|
||||
// ollama.chat({
|
||||
// model: modelName,
|
||||
// messages: messages,
|
||||
// stream: true,
|
||||
// // options: { num_predict: parseMaxTokensStr(thisConfig.maxTokens) } // this is max_tokens
|
||||
// })
|
||||
// .then(async stream => {
|
||||
// _setAborter(() => stream.abort())
|
||||
// // iterate through the stream
|
||||
// for await (const chunk of stream) {
|
||||
// const newText = chunk.message.content;
|
||||
|
||||
// // chunk.message.tool_calls[0].function.arguments
|
||||
|
||||
// fullText += newText;
|
||||
// onText({ newText, fullText });
|
||||
// }
|
||||
|
||||
// onFinalMessage({ fullText, tools: [] });
|
||||
|
||||
// })
|
||||
// // when error/fail
|
||||
// .catch((error) => {
|
||||
// onError({ message: error + '', fullError: error })
|
||||
// })
|
||||
|
||||
// };
|
||||
|
||||
|
||||
|
||||
// // ['codestral', 'qwen2.5-coder', 'qwen2.5-coder:0.5b', 'qwen2.5-coder:1.5b', 'qwen2.5-coder:3b', 'qwen2.5-coder:7b', 'qwen2.5-coder:14b', 'qwen2.5-coder:32b', 'codegemma', 'codegemma:2b', 'codegemma:7b', 'codellama', 'codellama:7b', 'codellama:13b', 'codellama:34b', 'codellama:70b', 'codellama:code', 'codellama:python', 'command-r', 'command-r:35b', 'command-r-plus', 'command-r-plus:104b', 'deepseek-coder-v2', 'deepseek-coder-v2:16b', 'deepseek-coder-v2:236b', 'falcon2', 'falcon2:11b', 'firefunction-v2', 'firefunction-v2:70b', 'gemma', 'gemma:2b', 'gemma:7b', 'gemma2', 'gemma2:2b', 'gemma2:9b', 'gemma2:27b', 'llama2', 'llama2:7b', 'llama2:13b', 'llama2:70b', 'llama3', 'llama3:8b', 'llama3:70b', 'llama3-chatqa', 'llama3-chatqa:8b', 'llama3-chatqa:70b', 'llama3-gradient', 'llama3-gradient:8b', 'llama3-gradient:70b', 'llama3.1', 'llama3.1:8b', 'llama3.1:70b', 'llama3.1:405b', 'llava', 'llava:7b', 'llava:13b', 'llava:34b', 'llava-llama3', 'llava-llama3:8b', 'llava-phi3', 'llava-phi3:3.8b', 'mistral', 'mistral:7b', 'mistral-large', 'mistral-large:123b', 'mistral-nemo', 'mistral-nemo:12b', 'mixtral', 'mixtral:8x7b', 'mixtral:8x22b', 'moondream', 'moondream:1.8b', 'openhermes', 'openhermes:v2.5', 'phi3', 'phi3:3.8b', 'phi3:14b', 'phi3.5', 'phi3.5:3.8b', 'qwen', 'qwen:7b', 'qwen:14b', 'qwen:32b', 'qwen:72b', 'qwen:110b', 'qwen2', 'qwen2:0.5b', 'qwen2:1.5b', 'qwen2:7b', 'qwen2:72b', 'smollm', 'smollm:135m', 'smollm:360m', 'smollm:1.7b',]
|
||||
|
|
@ -1,230 +0,0 @@
|
|||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import OpenAI from 'openai';
|
||||
import { _InternalModelListFnType, _InternalSendLLMFIMMessageFnType, _InternalSendLLMChatMessageFnType } from '../../common/llmMessageTypes.js';
|
||||
import { Model } from 'openai/resources/models.js';
|
||||
import { InternalToolInfo } from '../../common/toolsService.js';
|
||||
import { addSystemMessageAndToolSupport } from './preprocessLLMMessages.js';
|
||||
import { developerInfoOfModelName, developerInfoOfProviderName } from '../../common/voidSettingsTypes.js';
|
||||
import { isAToolName } from './postprocessToolCalls.js';
|
||||
// import { parseMaxTokensStr } from './util.js';
|
||||
|
||||
|
||||
// developer command - https://cdn.openai.com/spec/model-spec-2024-05-08.html#follow-the-chain-of-command
|
||||
// prompting - https://platform.openai.com/docs/guides/reasoning#advice-on-prompting
|
||||
|
||||
|
||||
export const toOpenAITool = (toolInfo: InternalToolInfo) => {
|
||||
const { name, description, params, required } = toolInfo
|
||||
return {
|
||||
type: 'function',
|
||||
function: {
|
||||
name: name,
|
||||
description: description,
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: params,
|
||||
required: required,
|
||||
}
|
||||
}
|
||||
} satisfies OpenAI.Chat.Completions.ChatCompletionTool
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
type NewParams = Pick<Parameters<_InternalSendLLMChatMessageFnType>[0] & Parameters<_InternalSendLLMFIMMessageFnType>[0], 'settingsOfProvider' | 'providerName'>
|
||||
const newOpenAI = ({ settingsOfProvider, providerName }: NewParams) => {
|
||||
|
||||
if (providerName === 'openAI') {
|
||||
const thisConfig = settingsOfProvider[providerName]
|
||||
return new OpenAI({
|
||||
apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true
|
||||
})
|
||||
}
|
||||
else if (providerName === 'ollama') {
|
||||
const thisConfig = settingsOfProvider[providerName]
|
||||
return new OpenAI({
|
||||
baseURL: `${thisConfig.endpoint}/v1`, apiKey: 'noop', dangerouslyAllowBrowser: true,
|
||||
})
|
||||
}
|
||||
else if (providerName === 'vLLM') {
|
||||
const thisConfig = settingsOfProvider[providerName]
|
||||
return new OpenAI({
|
||||
baseURL: `${thisConfig.endpoint}/v1`, apiKey: 'noop', dangerouslyAllowBrowser: true,
|
||||
})
|
||||
}
|
||||
else if (providerName === 'openRouter') {
|
||||
const thisConfig = settingsOfProvider[providerName]
|
||||
return new OpenAI({
|
||||
baseURL: 'https://openrouter.ai/api/v1', apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true,
|
||||
defaultHeaders: {
|
||||
'HTTP-Referer': 'https://voideditor.com', // Optional, for including your app on openrouter.ai rankings.
|
||||
'X-Title': 'Void Editor', // Optional. Shows in rankings on openrouter.ai.
|
||||
},
|
||||
})
|
||||
}
|
||||
else if (providerName === 'gemini') {
|
||||
const thisConfig = settingsOfProvider[providerName]
|
||||
return new OpenAI({
|
||||
baseURL: 'https://generativelanguage.googleapis.com/v1beta/openai', apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true,
|
||||
})
|
||||
}
|
||||
else if (providerName === 'deepseek') {
|
||||
const thisConfig = settingsOfProvider[providerName]
|
||||
return new OpenAI({
|
||||
baseURL: 'https://api.deepseek.com/v1', apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true,
|
||||
})
|
||||
}
|
||||
else if (providerName === 'openAICompatible') {
|
||||
const thisConfig = settingsOfProvider[providerName]
|
||||
return new OpenAI({
|
||||
baseURL: thisConfig.endpoint, apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true,
|
||||
})
|
||||
}
|
||||
else if (providerName === 'mistral') {
|
||||
const thisConfig = settingsOfProvider[providerName]
|
||||
return new OpenAI({
|
||||
baseURL: 'https://api.mistral.ai/v1', apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true,
|
||||
})
|
||||
}
|
||||
else if (providerName === 'groq') {
|
||||
const thisConfig = settingsOfProvider[providerName]
|
||||
return new OpenAI({
|
||||
baseURL: 'https://api.groq.com/openai/v1', apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true,
|
||||
})
|
||||
}
|
||||
else if (providerName === 'xAI') {
|
||||
const thisConfig = settingsOfProvider[providerName]
|
||||
return new OpenAI({
|
||||
baseURL: 'https://api.x.ai/v1', apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true,
|
||||
})
|
||||
}
|
||||
else {
|
||||
console.error(`sendOpenAICompatibleMsg: invalid providerName: ${providerName}`)
|
||||
throw new Error(`Void providerName was invalid: ${providerName}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// might not currently be used in the code
|
||||
export const openaiCompatibleList: _InternalModelListFnType<Model> = async ({ onSuccess: onSuccess_, onError: onError_, settingsOfProvider }) => {
|
||||
const onSuccess = ({ models }: { models: Model[] }) => {
|
||||
onSuccess_({ models })
|
||||
}
|
||||
|
||||
const onError = ({ error }: { error: string }) => {
|
||||
onError_({ error })
|
||||
}
|
||||
|
||||
try {
|
||||
const openai = newOpenAI({ providerName: 'openAICompatible', settingsOfProvider })
|
||||
|
||||
openai.models.list()
|
||||
.then(async (response) => {
|
||||
const models: Model[] = []
|
||||
models.push(...response.data)
|
||||
while (response.hasNextPage()) {
|
||||
models.push(...(await response.getNextPage()).data)
|
||||
}
|
||||
onSuccess({ models })
|
||||
})
|
||||
.catch((error) => {
|
||||
onError({ error: error + '' })
|
||||
})
|
||||
}
|
||||
catch (error) {
|
||||
onError({ error: error + '' })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
export const sendOpenAIFIM: _InternalSendLLMFIMMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName }) => {
|
||||
|
||||
|
||||
// openai.completions has a FIM parameter called `suffix`, but it's deprecated and only works for ~GPT 3 era models
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
// OpenAI, OpenRouter, OpenAICompatible
|
||||
export const sendOpenAIChat: _InternalSendLLMChatMessageFnType = ({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, aiInstructions, tools: tools_ }) => {
|
||||
|
||||
let fullText = ''
|
||||
const toolCallOfIndex: { [index: string]: { name: string, params: string, id: string } } = {}
|
||||
|
||||
const { overrideSettingsForAllModels } = developerInfoOfProviderName(providerName)
|
||||
const { supportsTools } = developerInfoOfModelName(modelName, overrideSettingsForAllModels)
|
||||
|
||||
const { messages } = addSystemMessageAndToolSupport(modelName, providerName, messages_, aiInstructions, { separateSystemMessage: false })
|
||||
|
||||
const tools = (supportsTools && ((tools_?.length ?? 0) !== 0)) ? tools_?.map(tool => toOpenAITool(tool)) : undefined
|
||||
|
||||
const openai: OpenAI = newOpenAI({ providerName, settingsOfProvider })
|
||||
const options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = {
|
||||
model: modelName,
|
||||
messages: messages,
|
||||
stream: true,
|
||||
tools: tools,
|
||||
tool_choice: tools ? 'auto' : undefined,
|
||||
parallel_tool_calls: tools ? false : undefined,
|
||||
}
|
||||
|
||||
openai.chat.completions
|
||||
.create(options)
|
||||
.then(async response => {
|
||||
_setAborter(() => response.controller.abort())
|
||||
|
||||
// when receive text
|
||||
for await (const chunk of response) {
|
||||
|
||||
// tool call
|
||||
for (const tool of chunk.choices[0]?.delta?.tool_calls ?? []) {
|
||||
const index = tool.index
|
||||
if (!toolCallOfIndex[index]) toolCallOfIndex[index] = { name: '', params: '', id: '' }
|
||||
toolCallOfIndex[index].name += tool.function?.name ?? ''
|
||||
toolCallOfIndex[index].params += tool.function?.arguments ?? '';
|
||||
toolCallOfIndex[index].id = tool.id ?? ''
|
||||
|
||||
}
|
||||
|
||||
// message
|
||||
let newText = ''
|
||||
newText += chunk.choices[0]?.delta?.content ?? ''
|
||||
fullText += newText;
|
||||
|
||||
onText({ newText, fullText });
|
||||
}
|
||||
onFinalMessage({
|
||||
fullText,
|
||||
toolCalls: Object.keys(toolCallOfIndex)
|
||||
.map(index => {
|
||||
const tool = toolCallOfIndex[index]
|
||||
if (isAToolName(tool.name))
|
||||
return { name: tool.name, id: tool.id, params: tool.params }
|
||||
return null
|
||||
})
|
||||
.filter(t => !!t)
|
||||
});
|
||||
})
|
||||
// when error/fail - this catches errors of both .create() and .then(for await)
|
||||
.catch(error => {
|
||||
if (error instanceof OpenAI.APIError && error.status === 401) {
|
||||
onError({ message: 'Invalid API key.', fullError: error });
|
||||
}
|
||||
else {
|
||||
onError({ message: error + '', fullError: error });
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
import { ToolName, toolNames } from '../../common/toolsService.js';
|
||||
|
||||
|
||||
|
||||
const toolNamesSet = new Set<string>(toolNames)
|
||||
|
||||
export const isAToolName = (toolName: string): toolName is ToolName => {
|
||||
const isAToolName = toolNamesSet.has(toolName)
|
||||
return isAToolName
|
||||
}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
|
||||
|
||||
import { LLMChatMessage } from '../../common/llmMessageTypes.js';
|
||||
import { developerInfoOfModelName, developerInfoOfProviderName, ProviderName } from '../../common/voidSettingsTypes.js';
|
||||
import { LLMChatMessage, LLMFIMMessage } from '../../common/llmMessageTypes.js';
|
||||
import { deepClone } from '../../../../../base/common/objects.js';
|
||||
|
||||
|
||||
|
|
@ -14,16 +13,24 @@ export const parseObject = (args: unknown) => {
|
|||
return {}
|
||||
}
|
||||
|
||||
// no matter whether the model supports a system message or not (or what format it supports), add it in some way
|
||||
// also take into account tools if the model doesn't support tool use
|
||||
export const addSystemMessageAndToolSupport = (modelName: string, providerName: ProviderName, messages_: LLMChatMessage[], aiInstructions: string, { separateSystemMessage }: { separateSystemMessage: boolean }): { separateSystemMessageStr?: string, messages: any[] } => {
|
||||
|
||||
const prepareMessages_cloneAndTrim = ({ messages: messages_ }: { messages: LLMChatMessage[] }) => {
|
||||
const messages = deepClone(messages_).map(m => ({ ...m, content: m.content.trim(), }))
|
||||
return { messages }
|
||||
}
|
||||
|
||||
const { overrideSettingsForAllModels } = developerInfoOfProviderName(providerName)
|
||||
const { supportsSystemMessage, supportsTools } = developerInfoOfModelName(modelName, overrideSettingsForAllModels)
|
||||
// no matter whether the model supports a system message or not (or what format it supports), add it in some way
|
||||
const prepareMessages_systemMessage = ({
|
||||
messages,
|
||||
aiInstructions,
|
||||
supportsSystemMessage,
|
||||
}: {
|
||||
messages: LLMChatMessage[],
|
||||
aiInstructions: string,
|
||||
supportsSystemMessage: false | 'system-role' | 'developer-role' | 'separated',
|
||||
})
|
||||
: { separateSystemMessageStr?: string, messages: any[] } => {
|
||||
|
||||
// 1. SYSTEM MESSAGE
|
||||
// find system messages and concatenate them
|
||||
let systemMessageStr = messages
|
||||
.filter(msg => msg.role === 'system')
|
||||
|
|
@ -33,7 +40,6 @@ export const addSystemMessageAndToolSupport = (modelName: string, providerName:
|
|||
if (aiInstructions)
|
||||
systemMessageStr = `${(systemMessageStr ? `${systemMessageStr}\n\n` : '')}GUIDELINES\n${aiInstructions}`
|
||||
|
||||
|
||||
let separateSystemMessageStr: string | undefined = undefined
|
||||
|
||||
// remove all system messages
|
||||
|
|
@ -49,11 +55,12 @@ export const addSystemMessageAndToolSupport = (modelName: string, providerName:
|
|||
if (systemMessageStr) {
|
||||
// if supports system message
|
||||
if (supportsSystemMessage) {
|
||||
if (separateSystemMessage)
|
||||
if (supportsSystemMessage === 'separated')
|
||||
separateSystemMessageStr = systemMessageStr
|
||||
else {
|
||||
newMessages.unshift({ role: supportsSystemMessage === 'developer' ? 'developer' : 'system', content: systemMessageStr }) // add new first message
|
||||
}
|
||||
else if (supportsSystemMessage === 'system-role')
|
||||
newMessages.unshift({ role: 'system', content: systemMessageStr }) // add new first message
|
||||
else if (supportsSystemMessage === 'developer-role')
|
||||
newMessages.unshift({ role: 'developer', content: systemMessageStr }) // add new first message
|
||||
}
|
||||
// if does not support system message
|
||||
else {
|
||||
|
|
@ -79,225 +86,265 @@ export const addSystemMessageAndToolSupport = (modelName: string, providerName:
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// 2. MAKE TOOLS FORMAT CORRECT in messages
|
||||
let finalMessages: any[]
|
||||
if (!supportsTools) {
|
||||
// do nothing
|
||||
finalMessages = newMessages
|
||||
}
|
||||
|
||||
// anthropic assistant message will have: https://docs.anthropic.com/en/docs/build-with-claude/tool-use#tool-use-examples
|
||||
// "content": [
|
||||
// {
|
||||
// "type": "text",
|
||||
// "text": "<thinking>I need to call the get_weather function, and the user wants SF, which is likely San Francisco, CA.</thinking>"
|
||||
// },
|
||||
// {
|
||||
// "type": "tool_use",
|
||||
// "id": "toolu_01A09q90qw90lq917835lq9",
|
||||
// "name": "get_weather",
|
||||
// "input": { "location": "San Francisco, CA", "unit": "celsius" }
|
||||
// }
|
||||
// ]
|
||||
|
||||
// anthropic user message response will be:
|
||||
// "content": [
|
||||
// {
|
||||
// "type": "tool_result",
|
||||
// "tool_use_id": "toolu_01A09q90qw90lq917835lq9",
|
||||
// "content": "15 degrees"
|
||||
// }
|
||||
// ]
|
||||
|
||||
|
||||
else if (providerName === 'anthropic') { // convert role:'tool' to anthropic's type
|
||||
const newMessagesTools: (
|
||||
Exclude<typeof newMessages[0], { role: 'assistant' | 'user' }> | {
|
||||
role: 'assistant',
|
||||
content: string | ({
|
||||
type: 'text';
|
||||
text: string;
|
||||
} | {
|
||||
type: 'tool_use';
|
||||
name: string;
|
||||
input: Record<string, any>;
|
||||
id: string;
|
||||
})[]
|
||||
} | {
|
||||
role: 'user',
|
||||
content: string | ({
|
||||
type: 'text';
|
||||
text: string;
|
||||
} | {
|
||||
type: 'tool_result';
|
||||
tool_use_id: string;
|
||||
content: string;
|
||||
})[]
|
||||
}
|
||||
)[] = newMessages;
|
||||
|
||||
|
||||
for (let i = 0; i < newMessagesTools.length; i += 1) {
|
||||
const currMsg = newMessagesTools[i]
|
||||
|
||||
if (currMsg.role !== 'tool') continue
|
||||
|
||||
const prevMsg = 0 <= i - 1 && i - 1 <= newMessagesTools.length ? newMessagesTools[i - 1] : undefined
|
||||
|
||||
if (prevMsg?.role === 'assistant') {
|
||||
if (typeof prevMsg.content === 'string') prevMsg.content = [{ type: 'text', text: prevMsg.content }]
|
||||
prevMsg.content.push({ type: 'tool_use', id: currMsg.id, name: currMsg.name, input: parseObject(currMsg.params) })
|
||||
}
|
||||
|
||||
// turn each tool into a user message with tool results at the end
|
||||
newMessagesTools[i] = {
|
||||
role: 'user',
|
||||
content: [
|
||||
...[{ type: 'tool_result', tool_use_id: currMsg.id, content: currMsg.content }] as const,
|
||||
...currMsg.content ? [{ type: 'text', text: currMsg.content }] as const : [],
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
finalMessages = newMessagesTools
|
||||
}
|
||||
|
||||
// openai assistant message will have: https://platform.openai.com/docs/guides/function-calling#function-calling-steps
|
||||
// "tool_calls":[
|
||||
// {
|
||||
// "type": "function",
|
||||
// "id": "call_12345xyz",
|
||||
// "function": {
|
||||
// "name": "get_weather",
|
||||
// "arguments": "{\"latitude\":48.8566,\"longitude\":2.3522}"
|
||||
// }
|
||||
// }]
|
||||
|
||||
// openai user response will be:
|
||||
// {
|
||||
// "role": "tool",
|
||||
// "tool_call_id": tool_call.id,
|
||||
// "content": str(result)
|
||||
// }
|
||||
|
||||
// treat all other providers like openai tool message for now
|
||||
else {
|
||||
|
||||
const newMessagesTools: (
|
||||
Exclude<typeof newMessages[0], { role: 'assistant' | 'tool' }> | {
|
||||
role: 'assistant',
|
||||
content: string;
|
||||
tool_calls?: {
|
||||
type: 'function';
|
||||
id: string;
|
||||
function: {
|
||||
name: string;
|
||||
arguments: string;
|
||||
}
|
||||
}[]
|
||||
} | {
|
||||
role: 'tool',
|
||||
id: string; // old val
|
||||
tool_call_id: string; // new val
|
||||
content: string;
|
||||
}
|
||||
)[] = [];
|
||||
|
||||
for (let i = 0; i < newMessages.length; i += 1) {
|
||||
const currMsg = newMessages[i]
|
||||
|
||||
if (currMsg.role !== 'tool') {
|
||||
newMessagesTools.push(currMsg)
|
||||
continue
|
||||
}
|
||||
|
||||
// edit previous assistant message to have called the tool
|
||||
const prevMsg = 0 <= i - 1 && i - 1 <= newMessagesTools.length ? newMessagesTools[i - 1] : undefined
|
||||
if (prevMsg?.role === 'assistant') {
|
||||
prevMsg.tool_calls = [{
|
||||
type: 'function',
|
||||
id: currMsg.id,
|
||||
function: {
|
||||
name: currMsg.name,
|
||||
arguments: JSON.stringify(currMsg.params)
|
||||
}
|
||||
}]
|
||||
}
|
||||
|
||||
// add the tool
|
||||
newMessagesTools.push({
|
||||
role: 'tool',
|
||||
id: currMsg.id,
|
||||
content: currMsg.content,
|
||||
tool_call_id: currMsg.id,
|
||||
})
|
||||
}
|
||||
finalMessages = newMessagesTools
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// 3. CROP MESSAGES SO EVERYTHING FITS IN CONTEXT
|
||||
// TODO!!!
|
||||
|
||||
|
||||
console.log('SYSMG', separateSystemMessage)
|
||||
console.log('FINAL MESSAGES', JSON.stringify(finalMessages, null, 2))
|
||||
|
||||
|
||||
return {
|
||||
separateSystemMessageStr,
|
||||
messages: finalMessages,
|
||||
}
|
||||
return { messages: newMessages, separateSystemMessageStr }
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// convert messages as if about to send to openai
|
||||
/*
|
||||
reference - https://platform.openai.com/docs/guides/function-calling#function-calling-steps
|
||||
openai MESSAGE (role=assistant):
|
||||
"tool_calls":[{
|
||||
"type": "function",
|
||||
"id": "call_12345xyz",
|
||||
"function": {
|
||||
"name": "get_weather",
|
||||
"arguments": "{\"latitude\":48.8566,\"longitude\":2.3522}"
|
||||
}]
|
||||
|
||||
openai RESPONSE (role=user):
|
||||
{ "role": "tool",
|
||||
"tool_call_id": tool_call.id,
|
||||
"content": str(result) }
|
||||
|
||||
also see
|
||||
openai on prompting - https://platform.openai.com/docs/guides/reasoning#advice-on-prompting
|
||||
openai on developer system message - https://cdn.openai.com/spec/model-spec-2024-05-08.html#follow-the-chain-of-command
|
||||
*/
|
||||
|
||||
const prepareMessages_tools_openai = ({ messages }: { messages: LLMChatMessage[], }) => {
|
||||
|
||||
const newMessages: (
|
||||
Exclude<LLMChatMessage, { role: 'assistant' | 'tool' }> | {
|
||||
role: 'assistant',
|
||||
content: string;
|
||||
tool_calls?: {
|
||||
type: 'function';
|
||||
id: string;
|
||||
function: {
|
||||
name: string;
|
||||
arguments: string;
|
||||
}
|
||||
}[]
|
||||
} | {
|
||||
role: 'tool',
|
||||
id: string; // old val
|
||||
tool_call_id: string; // new val
|
||||
content: string;
|
||||
}
|
||||
)[] = [];
|
||||
|
||||
for (let i = 0; i < messages.length; i += 1) {
|
||||
const currMsg = messages[i]
|
||||
|
||||
if (currMsg.role !== 'tool') {
|
||||
newMessages.push(currMsg)
|
||||
continue
|
||||
}
|
||||
|
||||
// edit previous assistant message to have called the tool
|
||||
const prevMsg = 0 <= i - 1 && i - 1 <= newMessages.length ? newMessages[i - 1] : undefined
|
||||
if (prevMsg?.role === 'assistant') {
|
||||
prevMsg.tool_calls = [{
|
||||
type: 'function',
|
||||
id: currMsg.id,
|
||||
function: {
|
||||
name: currMsg.name,
|
||||
arguments: JSON.stringify(currMsg.params)
|
||||
}
|
||||
}]
|
||||
}
|
||||
|
||||
// add the tool
|
||||
newMessages.push({
|
||||
role: 'tool',
|
||||
id: currMsg.id,
|
||||
content: currMsg.content,
|
||||
tool_call_id: currMsg.id,
|
||||
})
|
||||
}
|
||||
return { messages: newMessages }
|
||||
|
||||
}
|
||||
|
||||
|
||||
// convert messages as if about to send to anthropic
|
||||
/*
|
||||
https://docs.anthropic.com/en/docs/build-with-claude/tool-use#tool-use-examples
|
||||
anthropic MESSAGE (role=assistant):
|
||||
"content": [{
|
||||
"type": "text",
|
||||
"text": "<thinking>I need to call the get_weather function, and the user wants SF, which is likely San Francisco, CA.</thinking>"
|
||||
}, {
|
||||
"type": "tool_use",
|
||||
"id": "toolu_01A09q90qw90lq917835lq9",
|
||||
"name": "get_weather",
|
||||
"input": { "location": "San Francisco, CA", "unit": "celsius" }
|
||||
}]
|
||||
anthropic RESPONSE (role=user):
|
||||
"content": [{
|
||||
"type": "tool_result",
|
||||
"tool_use_id": "toolu_01A09q90qw90lq917835lq9",
|
||||
"content": "15 degrees"
|
||||
}]
|
||||
*/
|
||||
|
||||
const prepareMessages_tools_anthropic = ({ messages }: { messages: LLMChatMessage[], }) => {
|
||||
const newMessages: (
|
||||
Exclude<LLMChatMessage, { role: 'assistant' | 'user' }> | {
|
||||
role: 'assistant',
|
||||
content: string | ({
|
||||
type: 'text';
|
||||
text: string;
|
||||
} | {
|
||||
type: 'tool_use';
|
||||
name: string;
|
||||
input: Record<string, any>;
|
||||
id: string;
|
||||
})[]
|
||||
} | {
|
||||
role: 'user',
|
||||
content: string | ({
|
||||
type: 'text';
|
||||
text: string;
|
||||
} | {
|
||||
type: 'tool_result';
|
||||
tool_use_id: string;
|
||||
content: string;
|
||||
})[]
|
||||
}
|
||||
)[] = messages;
|
||||
|
||||
|
||||
for (let i = 0; i < newMessages.length; i += 1) {
|
||||
const currMsg = newMessages[i]
|
||||
|
||||
if (currMsg.role !== 'tool') continue
|
||||
|
||||
const prevMsg = 0 <= i - 1 && i - 1 <= newMessages.length ? newMessages[i - 1] : undefined
|
||||
|
||||
if (prevMsg?.role === 'assistant') {
|
||||
if (typeof prevMsg.content === 'string') prevMsg.content = [{ type: 'text', text: prevMsg.content }]
|
||||
prevMsg.content.push({ type: 'tool_use', id: currMsg.id, name: currMsg.name, input: parseObject(currMsg.params) })
|
||||
}
|
||||
|
||||
// turn each tool into a user message with tool results at the end
|
||||
newMessages[i] = {
|
||||
role: 'user',
|
||||
content: [
|
||||
...[{ type: 'tool_result', tool_use_id: currMsg.id, content: currMsg.content }] as const,
|
||||
...currMsg.content ? [{ type: 'text', text: currMsg.content }] as const : [],
|
||||
]
|
||||
}
|
||||
}
|
||||
return { messages: newMessages }
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const prepareMessages_tools = ({ messages, supportsTools }: { messages: LLMChatMessage[], supportsTools: false | 'anthropic-style' | 'openai-style' }) => {
|
||||
if (!supportsTools) {
|
||||
return { messages: messages }
|
||||
}
|
||||
else if (supportsTools === 'anthropic-style') {
|
||||
return prepareMessages_tools_anthropic({ messages })
|
||||
}
|
||||
else if (supportsTools === 'openai-style') {
|
||||
return prepareMessages_tools_openai({ messages })
|
||||
}
|
||||
else {
|
||||
throw 1
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/*
|
||||
Gemini has this, but they're openai-compat so we don't need to implement this
|
||||
gemini request:
|
||||
{ "role": "assistant",
|
||||
"content": null,
|
||||
"function_call": {
|
||||
"name": "get_weather",
|
||||
"arguments": {
|
||||
"latitude": 48.8566,
|
||||
"longitude": 2.3522
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
ACCORDING TO 4o: gemini: similar to openai, but function_call, and only 1 call per message (no id because only 1 message)
|
||||
gemini request: {
|
||||
"role": "assistant",
|
||||
"content": null,
|
||||
"function_call": {
|
||||
"name": "get_weather",
|
||||
"arguments": {
|
||||
"latitude": 48.8566,
|
||||
"longitude": 2.3522
|
||||
}
|
||||
}
|
||||
}
|
||||
gemini response:
|
||||
{
|
||||
"role": "assistant",
|
||||
"function_response": {
|
||||
"name": "get_weather",
|
||||
"response": {
|
||||
"temperature": "15°C",
|
||||
"condition": "Cloudy"
|
||||
{ "role": "assistant",
|
||||
"function_response": {
|
||||
"name": "get_weather",
|
||||
"response": {
|
||||
"temperature": "15°C",
|
||||
"condition": "Cloudy"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+ anthropic
|
||||
|
||||
+ openai-compat (4)
|
||||
+ gemini
|
||||
|
||||
ollama
|
||||
|
||||
|
||||
mistral: same as openai
|
||||
|
||||
*/
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export const prepareMessages = ({
|
||||
messages,
|
||||
aiInstructions,
|
||||
supportsSystemMessage,
|
||||
supportsTools,
|
||||
}: {
|
||||
messages: LLMChatMessage[],
|
||||
aiInstructions: string,
|
||||
supportsSystemMessage: false | 'system-role' | 'developer-role' | 'separated',
|
||||
supportsTools: false | 'anthropic-style' | 'openai-style',
|
||||
}) => {
|
||||
const { messages: messages1 } = prepareMessages_cloneAndTrim({ messages })
|
||||
const { messages: messages2, separateSystemMessageStr } = prepareMessages_systemMessage({ messages: messages1, aiInstructions, supportsSystemMessage })
|
||||
const { messages: messages3 } = prepareMessages_tools({ messages: messages2, supportsTools })
|
||||
return {
|
||||
messages: messages3 as any,
|
||||
separateSystemMessageStr
|
||||
} as const
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export const prepareFIMMessage = ({
|
||||
messages,
|
||||
aiInstructions,
|
||||
}: {
|
||||
messages: LLMFIMMessage,
|
||||
aiInstructions: string,
|
||||
}) => {
|
||||
|
||||
let prefix = `\
|
||||
${!aiInstructions ? '' : `\
|
||||
// Instructions:
|
||||
// Do not output an explanation. Try to avoid outputting comments. Only output the middle code.
|
||||
${aiInstructions.split('\n').map(line => `//${line}`).join('\n')}`}
|
||||
|
||||
${messages.prefix}`
|
||||
|
||||
const suffix = messages.suffix
|
||||
const stopTokens = messages.stopTokens
|
||||
const ret = { prefix, suffix, stopTokens, maxTokens: 300 } as const
|
||||
console.log('ret', ret)
|
||||
return ret
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,9 +6,7 @@
|
|||
import { SendLLMMessageParams, OnText, OnFinalMessage, OnError } from '../../common/llmMessageTypes.js';
|
||||
import { IMetricsService } from '../../common/metricsService.js';
|
||||
import { displayInfoOfProviderName } from '../../common/voidSettingsTypes.js';
|
||||
|
||||
import { sendAnthropicChat } from './anthropic.js';
|
||||
import { sendOpenAIChat } from './openai.js';
|
||||
import { sendLLMMessageToProviderImplementation } from './MODELS.js';
|
||||
|
||||
|
||||
export const sendLLMMessage = ({
|
||||
|
|
@ -35,6 +33,8 @@ export const sendLLMMessage = ({
|
|||
metricsService.capture(eventId, {
|
||||
providerName,
|
||||
modelName,
|
||||
customEndpointURL: settingsOfProvider[providerName]?.endpoint,
|
||||
numModelsAtEndpoint: settingsOfProvider[providerName]?.models?.length,
|
||||
...messagesType === 'chatMessages' ? {
|
||||
numMessages: messages_?.length,
|
||||
messagesShape: messages_?.map(msg => ({ role: msg.role, length: msg.content.length })),
|
||||
|
|
@ -56,9 +56,10 @@ export const sendLLMMessage = ({
|
|||
let _setAborter = (fn: () => void) => { _aborter = fn }
|
||||
let _didAbort = false
|
||||
|
||||
const onText: OnText = ({ newText, fullText }) => {
|
||||
const onText: OnText = (params) => {
|
||||
const { fullText } = params
|
||||
if (_didAbort) return
|
||||
onText_({ newText, fullText })
|
||||
onText_(params)
|
||||
_fullTextSoFar = fullText
|
||||
}
|
||||
|
||||
|
|
@ -74,7 +75,7 @@ export const sendLLMMessage = ({
|
|||
|
||||
// handle failed to fetch errors, which give 0 information by design
|
||||
if (error === 'TypeError: fetch failed')
|
||||
error = `Failed to fetch from ${displayInfoOfProviderName(providerName).title}. This likely means you specified the wrong endpoint in Void Settings, or your local model provider like Ollama is powered off.`
|
||||
error = `Failed to fetch from ${displayInfoOfProviderName(providerName).title}. This likely means you specified the wrong endpoint in Void's Settings, or your local model provider like Ollama is powered off.`
|
||||
|
||||
captureLLMEvent(`${loggingName} - Error`, { error })
|
||||
onError_({ message: error, fullError })
|
||||
|
|
@ -93,29 +94,27 @@ export const sendLLMMessage = ({
|
|||
else if (messagesType === 'FIMMessage')
|
||||
captureLLMEvent(`${loggingName} - Sending FIM`, {}) // TODO!!! add more metrics
|
||||
|
||||
|
||||
try {
|
||||
switch (providerName) {
|
||||
case 'openAI':
|
||||
case 'openRouter':
|
||||
case 'deepseek':
|
||||
case 'openAICompatible':
|
||||
case 'mistral':
|
||||
case 'ollama':
|
||||
case 'vLLM':
|
||||
case 'groq':
|
||||
case 'gemini':
|
||||
case 'xAI':
|
||||
if (messagesType === 'FIMMessage') onFinalMessage({ fullText: 'TODO - OpenAI API FIM', toolCalls: [] })
|
||||
else /* */ sendOpenAIChat({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, aiInstructions, tools });
|
||||
break;
|
||||
case 'anthropic':
|
||||
if (messagesType === 'FIMMessage') onFinalMessage({ fullText: 'TODO - Anthropic FIM', toolCalls: [] })
|
||||
else /* */ sendAnthropicChat({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, aiInstructions, tools });
|
||||
break;
|
||||
default:
|
||||
onError({ message: `Error: Void provider was "${providerName}", which is not recognized.`, fullError: null })
|
||||
break;
|
||||
const implementation = sendLLMMessageToProviderImplementation[providerName]
|
||||
if (!implementation) {
|
||||
onError({ message: `Error: Provider "${providerName}" not recognized.`, fullError: null })
|
||||
return
|
||||
}
|
||||
const { sendFIM, sendChat } = implementation
|
||||
if (messagesType === 'chatMessages') {
|
||||
sendChat({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, aiInstructions, tools })
|
||||
return
|
||||
}
|
||||
if (messagesType === 'FIMMessage') {
|
||||
if (sendFIM) {
|
||||
sendFIM({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, aiInstructions })
|
||||
return
|
||||
}
|
||||
onError({ message: `Error: This provider does not support Autocomplete yet.`, fullError: null })
|
||||
return
|
||||
}
|
||||
onError({ message: `Error: Message type "${messagesType}" not recognized.`, fullError: null })
|
||||
}
|
||||
|
||||
catch (error) {
|
||||
|
|
|
|||
|
|
@ -8,30 +8,42 @@
|
|||
|
||||
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, MainModelListParams, ModelListParams, EventModelListOnSuccessParams, EventModelListOnErrorParams, OllamaModelResponse, OpenaiCompatibleModelResponse, } from '../common/llmMessageTypes.js';
|
||||
import { EventLLMMessageOnTextParams, EventLLMMessageOnErrorParams, EventLLMMessageOnFinalMessageParams, MainSendLLMMessageParams, AbortRef, SendLLMMessageParams, MainLLMMessageAbortParams, ModelListParams, EventModelListOnSuccessParams, EventModelListOnErrorParams, OllamaModelResponse, VLLMModelResponse, MainModelListParams, } from '../common/llmMessageTypes.js';
|
||||
import { sendLLMMessage } from './llmMessage/sendLLMMessage.js'
|
||||
import { IMetricsService } from '../common/metricsService.js';
|
||||
import { ollamaList } from './llmMessage/ollama.js';
|
||||
import { openaiCompatibleList } from './llmMessage/openai.js';
|
||||
import { sendLLMMessageToProviderImplementation } from './llmMessage/MODELS.js';
|
||||
|
||||
// NODE IMPLEMENTATION - calls actual sendLLMMessage() and returns listeners to it
|
||||
|
||||
export class LLMMessageChannel implements IServerChannel {
|
||||
|
||||
// sendLLMMessage
|
||||
private readonly _onText_llm = new Emitter<EventLLMMessageOnTextParams>();
|
||||
private readonly _onFinalMessage_llm = new Emitter<EventLLMMessageOnFinalMessageParams>();
|
||||
private readonly _onError_llm = new Emitter<EventLLMMessageOnErrorParams>();
|
||||
private readonly llmMessageEmitters = {
|
||||
onText: new Emitter<EventLLMMessageOnTextParams>(),
|
||||
onFinalMessage: new Emitter<EventLLMMessageOnFinalMessageParams>(),
|
||||
onError: new Emitter<EventLLMMessageOnErrorParams>(),
|
||||
}
|
||||
|
||||
// abort
|
||||
private readonly _abortRefOfRequestId_llm: Record<string, AbortRef> = {}
|
||||
// aborters for above
|
||||
private readonly abortRefOfRequestId: Record<string, AbortRef> = {}
|
||||
|
||||
// ollamaList
|
||||
private readonly _onSuccess_ollama = new Emitter<EventModelListOnSuccessParams<OllamaModelResponse>>();
|
||||
private readonly _onError_ollama = new Emitter<EventModelListOnErrorParams<OllamaModelResponse>>();
|
||||
|
||||
// openaiCompatibleList
|
||||
private readonly _onSuccess_openAICompatible = new Emitter<EventModelListOnSuccessParams<OpenaiCompatibleModelResponse>>();
|
||||
private readonly _onError_openAICompatible = new Emitter<EventModelListOnErrorParams<OpenaiCompatibleModelResponse>>();
|
||||
// list
|
||||
private readonly listEmitters = {
|
||||
ollama: {
|
||||
success: new Emitter<EventModelListOnSuccessParams<OllamaModelResponse>>(),
|
||||
error: new Emitter<EventModelListOnErrorParams<OllamaModelResponse>>(),
|
||||
},
|
||||
vLLM: {
|
||||
success: new Emitter<EventModelListOnSuccessParams<VLLMModelResponse>>(),
|
||||
error: new Emitter<EventModelListOnErrorParams<VLLMModelResponse>>(),
|
||||
}
|
||||
} satisfies {
|
||||
[providerName: string]: {
|
||||
success: Emitter<EventModelListOnSuccessParams<any>>,
|
||||
error: Emitter<EventModelListOnErrorParams<any>>,
|
||||
}
|
||||
}
|
||||
|
||||
// stupidly, channels can't take in @IService
|
||||
constructor(
|
||||
|
|
@ -40,30 +52,17 @@ export class LLMMessageChannel implements IServerChannel {
|
|||
|
||||
// browser uses this to listen for changes
|
||||
listen(_: unknown, event: string): Event<any> {
|
||||
if (event === 'onText_llm') {
|
||||
return this._onText_llm.event;
|
||||
}
|
||||
else if (event === 'onFinalMessage_llm') {
|
||||
return this._onFinalMessage_llm.event;
|
||||
}
|
||||
else if (event === 'onError_llm') {
|
||||
return this._onError_llm.event;
|
||||
}
|
||||
else if (event === 'onSuccess_ollama') {
|
||||
return this._onSuccess_ollama.event;
|
||||
}
|
||||
else if (event === 'onError_ollama') {
|
||||
return this._onError_ollama.event;
|
||||
}
|
||||
else if (event === 'onSuccess_openAICompatible') {
|
||||
return this._onSuccess_openAICompatible.event;
|
||||
}
|
||||
else if (event === 'onError_openAICompatible') {
|
||||
return this._onError_openAICompatible.event;
|
||||
}
|
||||
else {
|
||||
throw new Error(`Event not found: ${event}`);
|
||||
}
|
||||
// text
|
||||
if (event === 'onText_sendLLMMessage') return this.llmMessageEmitters.onText.event;
|
||||
else if (event === 'onFinalMessage_sendLLMMessage') return this.llmMessageEmitters.onFinalMessage.event;
|
||||
else if (event === 'onError_sendLLMMessage') return this.llmMessageEmitters.onError.event;
|
||||
// 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 throw new Error(`Event not found: ${event}`);
|
||||
}
|
||||
|
||||
// browser uses this to call (see this.channel.call() in llmMessageService.ts for all usages)
|
||||
|
|
@ -78,8 +77,8 @@ export class LLMMessageChannel implements IServerChannel {
|
|||
else if (command === 'ollamaList') {
|
||||
this._callOllamaList(params)
|
||||
}
|
||||
else if (command === 'openAICompatibleList') {
|
||||
this._callOpenAICompatibleList(params)
|
||||
else if (command === 'vLLMList') {
|
||||
this._callVLLMList(params)
|
||||
}
|
||||
else {
|
||||
throw new Error(`Void sendLLM: command "${command}" not recognized.`)
|
||||
|
|
@ -94,47 +93,50 @@ export class LLMMessageChannel implements IServerChannel {
|
|||
private async _callSendLLMMessage(params: MainSendLLMMessageParams) {
|
||||
const { requestId } = params;
|
||||
|
||||
if (!(requestId in this._abortRefOfRequestId_llm))
|
||||
this._abortRefOfRequestId_llm[requestId] = { current: null }
|
||||
if (!(requestId in this.abortRefOfRequestId))
|
||||
this.abortRefOfRequestId[requestId] = { current: null }
|
||||
|
||||
const mainThreadParams: SendLLMMessageParams = {
|
||||
...params,
|
||||
onText: ({ newText, fullText }) => { this._onText_llm.fire({ requestId, newText, fullText }); },
|
||||
onFinalMessage: ({ fullText, toolCalls }) => { this._onFinalMessage_llm.fire({ requestId, fullText, toolCalls }); },
|
||||
onError: ({ message: error, fullError }) => { console.log('sendLLM: firing err'); this._onError_llm.fire({ requestId, message: error, fullError }); },
|
||||
abortRef: this._abortRefOfRequestId_llm[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.abortRefOfRequestId[requestId],
|
||||
}
|
||||
sendLLMMessage(mainThreadParams, this.metricsService);
|
||||
}
|
||||
|
||||
private _callAbort(params: MainLLMMessageAbortParams) {
|
||||
const { requestId } = params;
|
||||
if (!(requestId in this._abortRefOfRequestId_llm)) return
|
||||
this._abortRefOfRequestId_llm[requestId].current?.()
|
||||
delete this._abortRefOfRequestId_llm[requestId]
|
||||
}
|
||||
|
||||
private _callOllamaList(params: MainModelListParams<OllamaModelResponse>) {
|
||||
const { requestId } = params;
|
||||
|
||||
_callOllamaList = (params: MainModelListParams<OllamaModelResponse>) => {
|
||||
const { requestId } = params
|
||||
const emitters = this.listEmitters.ollama
|
||||
const mainThreadParams: ModelListParams<OllamaModelResponse> = {
|
||||
...params,
|
||||
onSuccess: ({ models }) => { this._onSuccess_ollama.fire({ requestId, models }); },
|
||||
onError: ({ error }) => { this._onError_ollama.fire({ requestId, error }); },
|
||||
onSuccess: (p) => { emitters.success.fire({ requestId, ...p }); },
|
||||
onError: (p) => { emitters.error.fire({ requestId, ...p }); },
|
||||
}
|
||||
ollamaList(mainThreadParams)
|
||||
sendLLMMessageToProviderImplementation.ollama.list(mainThreadParams)
|
||||
}
|
||||
|
||||
private _callOpenAICompatibleList(params: MainModelListParams<OpenaiCompatibleModelResponse>) {
|
||||
const { requestId } = params;
|
||||
|
||||
const mainThreadParams: ModelListParams<OpenaiCompatibleModelResponse> = {
|
||||
_callVLLMList = (params: MainModelListParams<VLLMModelResponse>) => {
|
||||
const { requestId } = params
|
||||
const emitters = this.listEmitters.vLLM
|
||||
const mainThreadParams: ModelListParams<VLLMModelResponse> = {
|
||||
...params,
|
||||
onSuccess: ({ models }) => { this._onSuccess_openAICompatible.fire({ requestId, models }); },
|
||||
onError: ({ error }) => { this._onError_openAICompatible.fire({ requestId, error }); },
|
||||
onSuccess: (p) => { emitters.success.fire({ requestId, ...p }); },
|
||||
onError: (p) => { emitters.error.fire({ requestId, ...p }); },
|
||||
}
|
||||
openaiCompatibleList(mainThreadParams)
|
||||
sendLLMMessageToProviderImplementation.vLLM.list(mainThreadParams)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
private _callAbort(params: MainLLMMessageAbortParams) {
|
||||
const { requestId } = params;
|
||||
if (!(requestId in this.abortRefOfRequestId)) return
|
||||
this.abortRefOfRequestId[requestId].current?.()
|
||||
delete this.abortRefOfRequestId[requestId]
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue