mirror of
https://github.com/voideditor/void
synced 2026-05-24 09:58:23 +00:00
tool use plugboard progress
This commit is contained in:
parent
152e605856
commit
8591d06244
12 changed files with 367 additions and 254 deletions
|
|
@ -65,6 +65,7 @@ export type ChatMessage =
|
|||
role: 'tool';
|
||||
name: string; // internal use
|
||||
params: string | null; // internal use
|
||||
tool_use_id: string; // apis require this
|
||||
content: string | null; // summary of the tool to the LLM
|
||||
displayContent: string | null; // text message of result
|
||||
}
|
||||
|
|
@ -111,10 +112,12 @@ const newThreadObject = () => {
|
|||
}
|
||||
|
||||
const THREAD_VERSION_KEY = 'void.chatThreadVersion'
|
||||
const THREAD_VERSION = 'v2'
|
||||
const LATEST_THREAD_VERSION = 'v2'
|
||||
|
||||
const THREAD_STORAGE_KEY = 'void.chatThreadStorage'
|
||||
|
||||
|
||||
type ChatMode = 'agent' | 'chat'
|
||||
export interface IChatThreadService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
|
|
@ -134,8 +137,8 @@ export interface IChatThreadService {
|
|||
|
||||
useFocusedStagingState(messageIdx?: number | undefined): readonly [StagingInfo, (stagingInfo: StagingInfo) => void];
|
||||
|
||||
editUserMessageAndStreamResponse(userMessage: string, messageIdx: number): Promise<void>;
|
||||
addUserMessageAndStreamResponse(userMessage: string): Promise<void>;
|
||||
editUserMessageAndStreamResponse({ userMessage, chatMode, messageIdx }: { userMessage: string, chatMode: ChatMode, messageIdx: number }): Promise<void>;
|
||||
addUserMessageAndStreamResponse({ userMessage, chatMode }: { userMessage: string, chatMode: ChatMode }): Promise<void>;
|
||||
cancelStreaming(threadId: string): void;
|
||||
dismissStreamError(threadId: string): void;
|
||||
|
||||
|
|
@ -182,7 +185,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
// always be in a thread
|
||||
this.openNewThread()
|
||||
|
||||
this._storageService.store(THREAD_VERSION_KEY, THREAD_VERSION, StorageScope.APPLICATION, StorageTarget.USER)
|
||||
this._storageService.store(THREAD_VERSION_KEY, LATEST_THREAD_VERSION, StorageScope.APPLICATION, StorageTarget.USER)
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -272,7 +275,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
|
||||
|
||||
|
||||
async addUserMessageAndStreamResponse(userMessage: string, stagingOverride?: StagingInfo | null) {
|
||||
async addUserMessageAndStreamResponse({ userMessage, chatMode, stagingOverride }: { userMessage: string, chatMode: ChatMode, stagingOverride?: StagingInfo | null }) {
|
||||
|
||||
const thread = this.getCurrentThread()
|
||||
const threadId = thread.id
|
||||
|
|
@ -293,14 +296,10 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
|
||||
|
||||
// agent loop
|
||||
|
||||
|
||||
let shouldContinue = false
|
||||
do {
|
||||
shouldContinue = false
|
||||
|
||||
console.log('Q')
|
||||
|
||||
let res_: () => void
|
||||
const awaitable = new Promise<void>((res, rej) => { res_ = res })
|
||||
|
||||
|
|
@ -310,9 +309,9 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
logging: { loggingName: `Agent` },
|
||||
messages: [
|
||||
{ role: 'system', content: chat_systemMessage },
|
||||
...this.getCurrentThread().messages.map(m => ({ role: m.role, content: m.content || '(empty model output)' })),
|
||||
...this.getCurrentThread().messages.map(m => ({ ...m, content: m.content || '(empty model output)' })),
|
||||
],
|
||||
tools: [voidTools['read_file']],
|
||||
tools: [voidTools['read_file']], // TODO!!!!! make this change on agent | chat | search
|
||||
|
||||
onText: ({ fullText }) => {
|
||||
this._setStreamState(threadId, { messageSoFar: fullText })
|
||||
|
|
@ -324,13 +323,13 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
else {
|
||||
for (const tool of tools) {
|
||||
if (!(tool.name in this._toolsService.toolFns)) {
|
||||
this._addMessageToThread(threadId, { role: 'tool', name: tool.name, params: tool.args, content: `Error: This tool was not recognized, so it was not called.`, displayContent: `Error: tool not recognized.`, })
|
||||
this._addMessageToThread(threadId, { role: 'tool', name: tool.name, params: tool.args, tool_use_id: tool.tool_use_id, content: `Error: This tool was not recognized, so it was not called.`, displayContent: `Error: tool not recognized.`, })
|
||||
}
|
||||
else {
|
||||
const toolName = tool.name as ToolName
|
||||
const toolResult = await this._toolsService.toolFns[toolName](JSON.parse(tool.args))
|
||||
const string = this._toolsService.toolResultToString[toolName](toolResult as any)
|
||||
this._addMessageToThread(threadId, { role: 'tool', name: tool.name, params: tool.args, content: string, displayContent: string, })
|
||||
const toolResult = await this._toolsService.toolFns[toolName](tool.args)
|
||||
const string = this._toolsService.toolResultToString[toolName](toolResult as any) // typescript is so bad it doesn't even couple the type of ToolResult with the type of the function being called here
|
||||
this._addMessageToThread(threadId, { role: 'tool', name: tool.name, params: tool.args, tool_use_id: tool.tool_use_id, content: string, displayContent: string, })
|
||||
shouldContinue = true
|
||||
}
|
||||
}
|
||||
|
|
@ -377,7 +376,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
}
|
||||
|
||||
|
||||
async editUserMessageAndStreamResponse(userMessage: string, messageIdx: number) {
|
||||
async editUserMessageAndStreamResponse({ userMessage, chatMode, messageIdx }: { userMessage: string, chatMode: ChatMode, messageIdx: number }) {
|
||||
|
||||
const thread = this.getCurrentThread()
|
||||
|
||||
|
|
@ -400,7 +399,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
}, true)
|
||||
|
||||
// re-add the message and stream it
|
||||
this.addUserMessageAndStreamResponse(userMessage, messageToReplace.staging)
|
||||
this.addUserMessageAndStreamResponse({ userMessage, chatMode, stagingOverride: messageToReplace.staging })
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ import * as dom from '../../../../base/browser/dom.js';
|
|||
import { Widget } from '../../../../base/browser/ui/widget.js';
|
||||
import { URI } from '../../../../base/common/uri.js';
|
||||
import { IConsistentEditorItemService, IConsistentItemService } from './helperServices/consistentItemService.js';
|
||||
import { voidPrefixAndSuffix, ctrlKStream_userMessage, ctrlKStream_systemMessage, fastApply_rewritewholething_userMessage, fastApply_rewritewholething_systemMessage, defaultQuickEditFimTags, searchReplace_userMessage, searchReplace_systemMessage } from './prompt/prompts.js';
|
||||
import { voidPrefixAndSuffix, ctrlKStream_userMessage, ctrlKStream_systemMessage, rewriteCode_userMessage, rewriteCode_systemMessage, defaultQuickEditFimTags, searchReplace_userMessage, searchReplace_systemMessage } from './prompt/prompts.js';
|
||||
|
||||
import { mountCtrlK } from './react/out/quick-edit-tsx/index.js'
|
||||
import { QuickEditPropsType } from './quickEditActions.js';
|
||||
|
|
@ -1415,9 +1415,9 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
let messages: LLMChatMessage[]
|
||||
|
||||
if (from === 'ClickApply') {
|
||||
const userContent = fastApply_rewritewholething_userMessage({ originalCode, applyStr: opts.applyStr, uri })
|
||||
const userContent = rewriteCode_userMessage({ originalCode, applyStr: opts.applyStr, uri })
|
||||
messages = [
|
||||
{ role: 'system', content: fastApply_rewritewholething_systemMessage, },
|
||||
{ role: 'system', content: rewriteCode_systemMessage, },
|
||||
{ role: 'user', content: userContent, }
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -187,7 +187,7 @@ export const chat_userMessage = async (instructions: string, selections: Staging
|
|||
|
||||
|
||||
|
||||
export const fastApply_rewritewholething_systemMessage = `\
|
||||
export const rewriteCode_systemMessage = `\
|
||||
You are a coding assistant that re-writes an entire file to make a change. You are given the original file \`ORIGINAL_FILE\` and a change \`CHANGE\`.
|
||||
|
||||
Directions:
|
||||
|
|
@ -199,7 +199,7 @@ Directions:
|
|||
|
||||
|
||||
|
||||
export const fastApply_rewritewholething_userMessage = ({ originalCode, applyStr, uri }: { originalCode: string, applyStr: string, uri: URI }) => {
|
||||
export const rewriteCode_userMessage = ({ originalCode, applyStr, uri }: { originalCode: string, applyStr: string, uri: URI }) => {
|
||||
|
||||
const language = filenameToVscodeLanguage(uri.fsPath) ?? ''
|
||||
|
||||
|
|
@ -311,7 +311,7 @@ Directions:
|
|||
4. The SEARCH/REPLACE blocks you generate will be applied immediately, and so they **MUST** produce a file that the user can run IMMEDIATELY.
|
||||
- Make sure you add all necessary imports.
|
||||
- Make sure the "final" code is complete and will not result in syntax/lint errors.
|
||||
5. Follow coding convention (spaces, semilcolons, comments, etc).
|
||||
5. Follow coding conventions of the user (spaces, semilcolons, comments, etc). If the user spaces or formats things a certain way, CONTINUE formatting it that way, even if you prefer otherwise.
|
||||
|
||||
## EXAMPLE 1
|
||||
ORIGINAL_FILE
|
||||
|
|
|
|||
|
|
@ -619,7 +619,7 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatM
|
|||
|
||||
// stream the edit
|
||||
const userMessage = textAreaRefState.value;
|
||||
await chatThreadsService.editUserMessageAndStreamResponse(userMessage, messageIdx)
|
||||
await chatThreadsService.editUserMessageAndStreamResponse({ userMessage, chatMode: 'agent', messageIdx })
|
||||
}
|
||||
|
||||
const onAbort = () => {
|
||||
|
|
@ -682,7 +682,7 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatM
|
|||
|
||||
chatbubbleContents = <ChatMarkdownRender string={chatMessage.displayContent ?? ''} chatMessageLocation={chatMessageLocation} />
|
||||
}
|
||||
else if (role === 'tool'){
|
||||
else if (role === 'tool') {
|
||||
chatbubbleContents = chatMessage.name
|
||||
}
|
||||
|
||||
|
|
@ -798,7 +798,7 @@ export const SidebarChat = () => {
|
|||
|
||||
// send message to LLM
|
||||
const userMessage = textAreaRef.current?.value ?? ''
|
||||
await chatThreadsService.addUserMessageAndStreamResponse(userMessage)
|
||||
await chatThreadsService.addUserMessageAndStreamResponse({ userMessage, chatMode: 'agent' })
|
||||
|
||||
setStaging({ ...staging, selections: [], }) // clear staging
|
||||
textAreaFnsRef.current?.setValue('')
|
||||
|
|
|
|||
|
|
@ -22,17 +22,28 @@ export const errorDetails = (fullError: Error | null): string | null => {
|
|||
}
|
||||
|
||||
export type OnText = (p: { newText: string, fullText: string }) => void
|
||||
export type OnFinalMessage = (p: { fullText: string, tools: { name: string, args: string }[] }) => void
|
||||
export type OnFinalMessage = (p: { fullText: string, tools: { name: string, args: string, tool_use_id: string, }[] }) => void
|
||||
export type OnError = (p: { message: string, fullError: Error | null }) => void
|
||||
export type AbortRef = { current: (() => void) | null }
|
||||
|
||||
export type LLMChatMessage = {
|
||||
role: 'system' | 'user' | 'assistant' | 'tool';
|
||||
role: 'system' | 'user';
|
||||
content: string;
|
||||
} | {
|
||||
role: 'tool';
|
||||
tool_use_id: string;
|
||||
content: string;
|
||||
} | {
|
||||
role: 'assistant',
|
||||
tool_calls?: { name: string, tool_use_id: string, params: string }[];
|
||||
content: string;
|
||||
}
|
||||
|
||||
|
||||
|
||||
export type _InternalLLMChatMessage = {
|
||||
role: 'user' | 'assistant';
|
||||
role: any;
|
||||
tool_use_id?: any;
|
||||
content: string;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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, displayInfoOfProviderName, defaultProviderSettings, developerInfoOfRecognizedModel, modelInfoOfAutodetectedModelNames } from './voidSettingsTypes.js';
|
||||
import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, VoidModelInfo, GlobalSettings, GlobalSettingName, defaultGlobalSettings, displayInfoOfProviderName, defaultProviderSettings, developerInfoOfModelName, modelInfoOfAutodetectedModelNames } from './voidSettingsTypes.js';
|
||||
|
||||
|
||||
const STORAGE_KEY = 'void.settingsServiceStorage'
|
||||
|
|
@ -334,7 +334,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
|||
if (existingIdx !== -1) return // if exists, do nothing
|
||||
const newModels = [
|
||||
...models,
|
||||
{ ...developerInfoOfRecognizedModel(modelName), modelName, isDefault: false, isHidden: false }
|
||||
{ ...developerInfoOfModelName(modelName), modelName, isDefault: false, isHidden: false }
|
||||
]
|
||||
this.setSettingOfProvider(providerName, 'models', newModels)
|
||||
|
||||
|
|
|
|||
|
|
@ -9,17 +9,25 @@ import { VoidSettingsState } from './voidSettingsService.js'
|
|||
|
||||
|
||||
// developer info used in sendLLMMessage
|
||||
export type VoidModelDeveloperInfo = {
|
||||
export type DeveloperInfoAtModel = {
|
||||
// USED:
|
||||
|
||||
// TODO!!! think tokens - deepseek
|
||||
|
||||
// TODO!!!!
|
||||
// UNUSED (coming soon):
|
||||
recognizedModelName: RecognizedModel, // used to show user if model was auto-recognized
|
||||
recognizedModelName: RecognizedModelName, // used to show user if model was auto-recognized
|
||||
supportsTools: boolean, // we will just do a string of tool use if it doesn't support
|
||||
supportsSystemMessage: 'system' | 'developer' | false, // if null, we will just do a string of system message
|
||||
supportsSystemMessage: 'developer' | 'system' | false, // if null, we will just do a string of system message
|
||||
supportsAutocompleteFIM: boolean, // we will just do a description of FIM if it doens't support <|fim_hole|>
|
||||
supportsStreaming: boolean, // (o1 does NOT) we will just dump the final result if doesn't support it
|
||||
maxTokens: number, // required, DEFAULT is Infinity
|
||||
maxTokens: number, // required
|
||||
}
|
||||
|
||||
export type DeveloperInfoAtProvider = {
|
||||
separateSystemMessage?: boolean;
|
||||
toolsGoInRole?: boolean; // whether to do {role:'tool'} or {role:'user' tool:...}
|
||||
modelOverrides?: 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)
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -31,7 +39,7 @@ export type VoidModelInfo = { // <-- STATEFUL
|
|||
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
|
||||
} & VoidModelDeveloperInfo
|
||||
} & DeveloperInfoAtModel
|
||||
|
||||
|
||||
|
||||
|
|
@ -62,131 +70,155 @@ export const recognizedModels = [
|
|||
|
||||
] as const
|
||||
|
||||
type RecognizedModelName = (typeof recognizedModels)[number] | '<GENERAL>'
|
||||
|
||||
|
||||
|
||||
type RecognizedModel = (typeof recognizedModels)[number] | '<GENERAL>'
|
||||
|
||||
|
||||
// const modelCapabilities: { [recognizedModel in RecognizedModel]: ({ }) => string } = {
|
||||
// 'OpenAI 4o': {
|
||||
// template: ({ prefix, suffix, }: { prefix: string; suffix: string; }) => `\
|
||||
// `
|
||||
// }
|
||||
// }
|
||||
|
||||
export function getRecognizedModel(modelName: string): RecognizedModel {
|
||||
export function recognizedModelOfModelName(modelName: string): RecognizedModelName {
|
||||
const lower = modelName.toLowerCase();
|
||||
|
||||
if (lower.includes('gpt-4o')) {
|
||||
if (lower.includes('gpt-4o'))
|
||||
return 'OpenAI 4o';
|
||||
}
|
||||
if (lower.includes('claude')) {
|
||||
if (lower.includes('claude'))
|
||||
return 'Anthropic Claude';
|
||||
}
|
||||
if (lower.includes('llama')) {
|
||||
if (lower.includes('llama'))
|
||||
return 'Llama 3.x';
|
||||
}
|
||||
if (lower.includes('qwen2.5-coder')) {
|
||||
if (lower.includes('qwen2.5-coder'))
|
||||
return 'Alibaba Qwen2.5 Coder Instruct';
|
||||
}
|
||||
if (lower.includes('mistral')) {
|
||||
if (lower.includes('mistral'))
|
||||
return 'Mistral Codestral';
|
||||
}
|
||||
// Check for "o1" or "o3"
|
||||
if (/\bo1\b/.test(lower) || /\bo3\b/.test(lower)) {
|
||||
if (/\bo1\b/.test(lower) || /\bo3\b/.test(lower)) // o1, o3
|
||||
return 'OpenAI o1, o3';
|
||||
}
|
||||
if (lower.includes('deepseek-r1') || lower.includes('deepseek-reasoner')) {
|
||||
if (lower.includes('deepseek-r1') || lower.includes('deepseek-reasoner'))
|
||||
return 'Deepseek R1';
|
||||
}
|
||||
if (lower.includes('deepseek'))
|
||||
return 'Deepseek Chat'
|
||||
|
||||
// Fallback:
|
||||
return '<GENERAL>';
|
||||
}
|
||||
|
||||
|
||||
const developerInfoAtProvider: { [providerName in ProviderName]: DeveloperInfoAtProvider } = {
|
||||
'anthropic': {
|
||||
separateSystemMessage: true,
|
||||
toolsGoInRole: false,
|
||||
modelOverrides: {
|
||||
supportsTools: true,
|
||||
}
|
||||
},
|
||||
'deepseek': {
|
||||
separateSystemMessage: true,
|
||||
},
|
||||
'openAI': {
|
||||
separateSystemMessage: false,
|
||||
toolsGoInRole: true,
|
||||
},
|
||||
'gemini': {
|
||||
separateSystemMessage: true,
|
||||
toolsGoInRole: false
|
||||
},
|
||||
'mistral': {
|
||||
separateSystemMessage: true,
|
||||
},
|
||||
'groq': {
|
||||
separateSystemMessage: true,
|
||||
},
|
||||
'ollama': {
|
||||
separateSystemMessage: false,
|
||||
},
|
||||
'openRouter': {
|
||||
separateSystemMessage: true,
|
||||
},
|
||||
'openAICompatible': {
|
||||
separateSystemMessage: true,
|
||||
},
|
||||
}
|
||||
export const developerInfoOfProviderName = (providerName: ProviderName): Partial<DeveloperInfoAtProvider> => {
|
||||
return developerInfoAtProvider[providerName] ?? {}
|
||||
}
|
||||
|
||||
export const developerInfoOfRecognizedModel = (modelName: string) => {
|
||||
const devInfo: { [recognizedModel in RecognizedModel]: Omit<VoidModelDeveloperInfo, 'recognizedModelName'> } = {
|
||||
'OpenAI 4o': {
|
||||
supportsSystemMessage: false,
|
||||
supportsTools: false,
|
||||
supportsAutocompleteFIM: false,
|
||||
supportsStreaming: false,
|
||||
maxTokens: 4096,
|
||||
},
|
||||
|
||||
'Anthropic Claude': {
|
||||
supportsSystemMessage: false,
|
||||
supportsTools: false,
|
||||
supportsAutocompleteFIM: false,
|
||||
supportsStreaming: false,
|
||||
maxTokens: 4096,
|
||||
},
|
||||
|
||||
'Llama 3.x': {
|
||||
supportsSystemMessage: false,
|
||||
supportsTools: false,
|
||||
supportsAutocompleteFIM: false,
|
||||
supportsStreaming: false,
|
||||
maxTokens: 4096,
|
||||
},
|
||||
|
||||
'Deepseek Chat': {
|
||||
supportsSystemMessage: false,
|
||||
supportsTools: false,
|
||||
supportsAutocompleteFIM: false,
|
||||
supportsStreaming: false,
|
||||
maxTokens: 4096,
|
||||
},
|
||||
// providerName is optional, but gives some extra fallbacks if provided
|
||||
const developerInfoOfRecognizedModelName: { [recognizedModel in RecognizedModelName]: Omit<DeveloperInfoAtModel, 'recognizedModelName'> } = {
|
||||
'OpenAI 4o': {
|
||||
supportsSystemMessage: false,
|
||||
supportsTools: false,
|
||||
supportsAutocompleteFIM: false,
|
||||
supportsStreaming: false,
|
||||
maxTokens: 4096,
|
||||
},
|
||||
|
||||
'Alibaba Qwen2.5 Coder Instruct': {
|
||||
supportsSystemMessage: false,
|
||||
supportsTools: false,
|
||||
supportsAutocompleteFIM: false,
|
||||
supportsStreaming: false,
|
||||
maxTokens: 4096,
|
||||
},
|
||||
'Anthropic Claude': {
|
||||
supportsSystemMessage: false,
|
||||
supportsTools: false,
|
||||
supportsAutocompleteFIM: false,
|
||||
supportsStreaming: false,
|
||||
maxTokens: 4096,
|
||||
},
|
||||
|
||||
'Mistral Codestral': {
|
||||
supportsSystemMessage: false,
|
||||
supportsTools: false,
|
||||
supportsAutocompleteFIM: false,
|
||||
supportsStreaming: false,
|
||||
maxTokens: 4096,
|
||||
},
|
||||
'Llama 3.x': {
|
||||
supportsSystemMessage: false,
|
||||
supportsTools: false,
|
||||
supportsAutocompleteFIM: false,
|
||||
supportsStreaming: false,
|
||||
maxTokens: 4096,
|
||||
},
|
||||
|
||||
'OpenAI o1, o3': {
|
||||
supportsSystemMessage: false,
|
||||
supportsTools: false,
|
||||
supportsAutocompleteFIM: false,
|
||||
supportsStreaming: false,
|
||||
maxTokens: 4096,
|
||||
},
|
||||
'Deepseek Chat': {
|
||||
supportsSystemMessage: false,
|
||||
supportsTools: false,
|
||||
supportsAutocompleteFIM: false,
|
||||
supportsStreaming: false,
|
||||
maxTokens: 4096,
|
||||
},
|
||||
|
||||
'Deepseek R1': {
|
||||
supportsSystemMessage: false,
|
||||
supportsTools: false,
|
||||
supportsAutocompleteFIM: false,
|
||||
supportsStreaming: false,
|
||||
maxTokens: 4096,
|
||||
},
|
||||
'Alibaba Qwen2.5 Coder Instruct': {
|
||||
supportsSystemMessage: false,
|
||||
supportsTools: false,
|
||||
supportsAutocompleteFIM: false,
|
||||
supportsStreaming: false,
|
||||
maxTokens: 4096,
|
||||
},
|
||||
|
||||
'<GENERAL>': {
|
||||
supportsSystemMessage: false,
|
||||
supportsTools: false,
|
||||
supportsAutocompleteFIM: false,
|
||||
supportsStreaming: false,
|
||||
maxTokens: 4096,
|
||||
},
|
||||
}
|
||||
'Mistral Codestral': {
|
||||
supportsSystemMessage: false,
|
||||
supportsTools: false,
|
||||
supportsAutocompleteFIM: false,
|
||||
supportsStreaming: false,
|
||||
maxTokens: 4096,
|
||||
},
|
||||
|
||||
const recognizedModelName = getRecognizedModel(modelName)
|
||||
'OpenAI o1, o3': {
|
||||
supportsSystemMessage: false,
|
||||
supportsTools: false,
|
||||
supportsAutocompleteFIM: false,
|
||||
supportsStreaming: false,
|
||||
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,
|
||||
...devInfo[recognizedModelName],
|
||||
...developerInfoOfRecognizedModelName[recognizedModelName],
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -202,7 +234,7 @@ export const modelInfoOfDefaultModelNames = (defaultModelNames: string[]): VoidM
|
|||
isDefault: true,
|
||||
isAutodetected: false,
|
||||
isHidden: defaultModelNames.length >= 10, // hide all models if there are a ton of them, and make user enable them individually
|
||||
...developerInfoOfRecognizedModel(modelName),
|
||||
...developerInfoOfModelName(modelName),
|
||||
}))
|
||||
}
|
||||
|
||||
|
|
@ -219,7 +251,7 @@ export const modelInfoOfAutodetectedModelNames = (defaultModelNames: string[], o
|
|||
isDefault: true,
|
||||
isAutodetected: true,
|
||||
isHidden: !!existingModelsMap[modelName]?.isHidden,
|
||||
...developerInfoOfRecognizedModel(modelName)
|
||||
...developerInfoOfModelName(modelName)
|
||||
}))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -45,7 +45,8 @@ export const sendAnthropicChat: _InternalSendLLMChatMessageFnType = ({ messages,
|
|||
messages: messages,
|
||||
model: modelName,
|
||||
max_tokens: maxTokens,
|
||||
tools: tools?.map(tool => toAnthropicTool(tool))
|
||||
tools: tools?.map(tool => toAnthropicTool(tool)),
|
||||
tool_choice: { type: 'auto', disable_parallel_tool_use: true } // one tool use at a time
|
||||
})
|
||||
|
||||
|
||||
|
|
@ -77,7 +78,7 @@ export const sendAnthropicChat: _InternalSendLLMChatMessageFnType = ({ messages,
|
|||
stream.on('finalMessage', (response) => {
|
||||
// stringify the response's content
|
||||
const content = response.content.map(c => c.type === 'text' ? c.text : '').join('\n\n')
|
||||
const tools = response.content.map(c => c.type === 'tool_use' ? { name: c.name, args: JSON.stringify(c.input) } : null).filter(c => !!c)
|
||||
const tools = response.content.map(c => c.type === 'tool_use' ? { name: c.name, args: JSON.stringify(c.input), tool_use_id: c.id } : null).filter(c => !!c)
|
||||
|
||||
onFinalMessage({ fullText: content, tools })
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,42 +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 });
|
||||
})
|
||||
|
||||
|
||||
};
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { 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 });
|
||||
})
|
||||
}
|
||||
|
|
@ -64,6 +64,18 @@ const newOpenAI = ({ settingsOfProvider, providerName }: NewParams) => {
|
|||
baseURL: thisConfig.endpoint, apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true
|
||||
})
|
||||
}
|
||||
else if (providerName === 'mistral') {
|
||||
const thisConfig = settingsOfProvider.mistral
|
||||
return new OpenAI({
|
||||
baseURL: 'https://api.mistral.ai/v1', apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true,
|
||||
})
|
||||
}
|
||||
else if (providerName === 'groq') {
|
||||
const thisConfig = settingsOfProvider.groq
|
||||
return new OpenAI({
|
||||
baseURL: '"https://api.groq.com/openai/v1"', apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true,
|
||||
})
|
||||
}
|
||||
else {
|
||||
console.error(`sendOpenAIMsg: invalid providerName: ${providerName}`)
|
||||
throw new Error(`providerName was invalid: ${providerName}`)
|
||||
|
|
@ -167,4 +179,4 @@ export const sendOpenAIChat: _InternalSendLLMChatMessageFnType = ({ messages, on
|
|||
}
|
||||
})
|
||||
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,42 +10,189 @@ import { sendAnthropicChat } from './anthropic.js';
|
|||
import { sendOllamaFIM, sendOllamaChat } from './ollama.js';
|
||||
import { sendOpenAIChat } from './openai.js';
|
||||
import { sendGeminiChat } from './gemini.js';
|
||||
import { sendGroqChat } from './groq.js';
|
||||
import { sendMistralChat } from './mistral.js';
|
||||
import { displayInfoOfProviderName } from '../../common/voidSettingsTypes.js';
|
||||
import { developerInfoOfModelName, developerInfoOfProviderName, displayInfoOfProviderName, ProviderName, recognizedModelOfModelName } from '../../common/voidSettingsTypes.js';
|
||||
|
||||
|
||||
const cleanChatMessages = (modelName: string, providerName: ProviderName, messages: LLMChatMessage[]): { separateSystemMessageStr?: string, messages: _InternalLLMChatMessage[] } => {
|
||||
const recognizedModel = recognizedModelOfModelName(modelName)
|
||||
const { separateSystemMessage, toolsGoInRole, modelOverrides } = developerInfoOfProviderName(providerName)
|
||||
|
||||
const { supportsSystemMessage, maxTokens, /* supportsTools, supportsAutocompleteFIM, supportsStreaming */ } = developerInfoOfModelName(recognizedModel, modelOverrides)
|
||||
|
||||
|
||||
const cleanChatMessages = (messages: LLMChatMessage[]): _InternalLLMChatMessage[] => {
|
||||
// trim message content (Anthropic and other providers give an error if there is trailing whitespace)
|
||||
messages = messages.map(m => ({ ...m, content: m.content.trim() }))
|
||||
|
||||
|
||||
// 1. SYSTEM MESSAGE
|
||||
// find system messages and concatenate them
|
||||
const systemMessage = messages
|
||||
const systemMessageStr = messages
|
||||
.filter(msg => msg.role === 'system')
|
||||
.map(msg => msg.content)
|
||||
.join('\n') || undefined;
|
||||
|
||||
// remove all system messages
|
||||
const noSystemMessages = messages
|
||||
.filter(msg => msg.role !== 'system') as _InternalLLMChatMessage[]
|
||||
let separateSystemMessageStr = undefined
|
||||
|
||||
// add system mesasges to first message (should be a user message)
|
||||
if (systemMessage && (noSystemMessages.length !== 0)) {
|
||||
const newFirstMessage = {
|
||||
role: noSystemMessages[0].role,
|
||||
content: (''
|
||||
+ '<SYSTEM_MESSAGE>\n'
|
||||
+ systemMessage
|
||||
+ '\n'
|
||||
+ '</SYSTEM_MESSAGE>\n'
|
||||
+ noSystemMessages[0].content
|
||||
)
|
||||
// remove all system messages
|
||||
const noSystemMessages: _InternalLLMChatMessage[] = messages.filter(msg => msg.role !== 'system')
|
||||
|
||||
if (systemMessageStr) {
|
||||
// if supports system message
|
||||
if (supportsSystemMessage) {
|
||||
if (separateSystemMessage)
|
||||
separateSystemMessageStr = systemMessageStr
|
||||
else {
|
||||
noSystemMessages.unshift({ role: supportsSystemMessage, content: systemMessageStr }) // add new first message
|
||||
}
|
||||
}
|
||||
// if does not support system message
|
||||
else {
|
||||
if (supportsSystemMessage) {
|
||||
if (noSystemMessages.length === 0)
|
||||
noSystemMessages.push({ role: 'user', content: systemMessageStr })
|
||||
// add system mesasges to first message (should be a user message)
|
||||
else {
|
||||
const newFirstMessage = {
|
||||
role: noSystemMessages[0].role,
|
||||
content: (''
|
||||
+ '<SYSTEM_MESSAGE>\n'
|
||||
+ systemMessageStr
|
||||
+ '\n'
|
||||
+ '</SYSTEM_MESSAGE>\n'
|
||||
+ noSystemMessages[0].content
|
||||
)
|
||||
}
|
||||
noSystemMessages.splice(0, 1) // delete first message
|
||||
noSystemMessages.unshift(newFirstMessage) // add new first message
|
||||
}
|
||||
}
|
||||
}
|
||||
noSystemMessages.splice(0, 1) // delete first message
|
||||
noSystemMessages.unshift(newFirstMessage) // add new first message
|
||||
}
|
||||
|
||||
return noSystemMessages
|
||||
// 2. TOOLS
|
||||
|
||||
const newMessages = noSystemMessages;
|
||||
|
||||
if (toolsGoInRole) {
|
||||
let index = 0;
|
||||
while (index < newMessages.length) {
|
||||
|
||||
// merge tool with the previous assistant and the following user message
|
||||
|
||||
// take prev message and add
|
||||
/*
|
||||
openai assistant message will have: https://platform.openai.com/docs/guides/function-calling#function-calling-steps
|
||||
"tool_calls":[
|
||||
{
|
||||
"id": "call_12345xyz",
|
||||
"type": "function",
|
||||
"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)
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+ anthropic
|
||||
|
||||
+ openai-compat (4)
|
||||
+ gemini
|
||||
|
||||
ollama
|
||||
|
||||
|
||||
mistral: same as openai
|
||||
|
||||
*/
|
||||
|
||||
|
||||
if (newMessages[index].role === 'tool') {
|
||||
const toolMessage = newMessages[index];
|
||||
const assistantMessage = newMessages[index - 1];
|
||||
const userMessage = newMessages[index + 1];
|
||||
|
||||
// while ((toolIndex = newMessages.findIndex((msg, idx) => idx > toolIndex && msg.role === 'tool')) !== -1) {
|
||||
|
||||
// tool_use goes in assistant
|
||||
if (assistantMessage?.role === 'assistant') {
|
||||
assistantMessage.tool_use += `\n${toolMessage.content}`;
|
||||
}
|
||||
|
||||
// tool_result goes in user
|
||||
if (userMessage?.role === 'user') {
|
||||
|
||||
userMessage.content = `${toolMessage.content}\n${userMessage.content}`;
|
||||
}
|
||||
|
||||
// Remove the tool message after merging its content
|
||||
newMessages.splice(index, 1);
|
||||
} else {
|
||||
index++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
return {
|
||||
separateSystemMessageStr,
|
||||
messages: newMessages
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -68,11 +215,14 @@ export const sendLLMMessage = ({
|
|||
) => {
|
||||
|
||||
let messagesArr: _InternalLLMChatMessage[] = []
|
||||
|
||||
// TODO!!! move this to the actual providers
|
||||
if (messagesType === 'chatMessages') {
|
||||
messagesArr = cleanChatMessages([
|
||||
const { messages: cleanedMessages, separateSystemMessageStr } = cleanChatMessages(modelName, providerName, [
|
||||
{ role: 'system', content: aiInstructions },
|
||||
...messages_
|
||||
])
|
||||
messagesArr = cleanedMessages
|
||||
}
|
||||
|
||||
// only captures number of messages and message "shape", no actual code, instructions, prompts, etc
|
||||
|
|
@ -141,6 +291,8 @@ export const sendLLMMessage = ({
|
|||
case 'openRouter':
|
||||
case 'deepseek':
|
||||
case 'openAICompatible':
|
||||
case 'mistral':
|
||||
case 'groq':
|
||||
if (messagesType === 'FIMMessage') onFinalMessage({ fullText: 'TODO - OpenAI FIM', tools: [] })
|
||||
else /* */ sendOpenAIChat({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, tools });
|
||||
break;
|
||||
|
|
@ -156,14 +308,6 @@ export const sendLLMMessage = ({
|
|||
if (messagesType === 'FIMMessage') onFinalMessage({ fullText: 'TODO - Gemini FIM', tools: [] })
|
||||
else /* */ sendGeminiChat({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, tools });
|
||||
break;
|
||||
case 'groq':
|
||||
if (messagesType === 'FIMMessage') onFinalMessage({ fullText: 'TODO - Groq FIM', tools: [] })
|
||||
else /* */ sendGroqChat({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, tools });
|
||||
break;
|
||||
case 'mistral':
|
||||
if (messagesType === 'FIMMessage') onFinalMessage({ fullText: 'TODO - Mistral FIM', tools: [] })
|
||||
else /* */ sendMistralChat({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, tools });
|
||||
break;
|
||||
default:
|
||||
onError({ message: `Error: Void provider was "${providerName}", which is not recognized.`, fullError: null })
|
||||
break;
|
||||
|
|
|
|||
Loading…
Reference in a new issue