tool use plugboard progress

This commit is contained in:
Andrew Pareles 2025-02-15 19:23:15 -08:00
parent 152e605856
commit 8591d06244
12 changed files with 367 additions and 254 deletions

View file

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

View file

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

View file

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

View 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('')

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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