diff --git a/extensions/void/src/common/llm/index.ts b/extensions/void/src/common/llm/index.ts new file mode 100644 index 00000000..577c24c3 --- /dev/null +++ b/extensions/void/src/common/llm/index.ts @@ -0,0 +1,49 @@ +// Import message sending functions for different LLM providers +import { sendAnthropicMsg } from './providers/anthropic' +import { sendGeminiMsg } from './providers/gemini' +import { sendOpenAIMsg } from './providers/openai' +import { sendOllamaMsg } from './providers/ollama' +import { sendGreptileMsg } from './providers/greptile' +import { LLMMessage, OnText, OnFinalMessage, AbortRef } from './types' +import { VoidConfig } from '../../webviews/common/contextForConfig' + +// Main function to send messages to LLM providers +export const sendLLMMessage = ({ + messages, + onText, + onFinalMessage, + onError, + voidConfig, + abortRef +}: { + messages: LLMMessage[], // Array of messages to send + onText: OnText, // Callback for receiving text chunks + onFinalMessage: (fullText: string) => void, // Callback for final message + onError: (error: string) => void, // Error handling callback + voidConfig: VoidConfig | null, // Configuration object + abortRef: AbortRef, // Reference for aborting requests +}) => { + // Return early if no config is provided + if (!voidConfig) return + + // Trim whitespace from all message contents + messages = messages.map(m => ({ ...m, content: m.content.trim() })) + + // Route message to appropriate provider based on configuration + switch (voidConfig.default.whichApi) { + case 'anthropic': + return sendAnthropicMsg({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }) + case 'openAI': + case 'openRouter': + case 'openAICompatible': + return sendOpenAIMsg({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }) + case 'gemini': + return sendGeminiMsg({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }) + case 'ollama': + return sendOllamaMsg({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }) + case 'greptile': + return sendGreptileMsg({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }) + default: + onError(`Error: whichApi was ${voidConfig.default.whichApi}, which is not recognized!`) + } +} diff --git a/extensions/void/src/common/llm/providers/anthropic.ts b/extensions/void/src/common/llm/providers/anthropic.ts new file mode 100644 index 00000000..9a058c4f --- /dev/null +++ b/extensions/void/src/common/llm/providers/anthropic.ts @@ -0,0 +1,66 @@ +import Anthropic from '@anthropic-ai/sdk' +import { SendLLMMessageParams, LLMMessageAnthropic } from '../types' +import { parseMaxTokensStr } from '../utils' + +export const sendAnthropicMsg = ({ + messages, + onText, + onFinalMessage, + onError, + voidConfig +}: SendLLMMessageParams) => { + const anthropic = new Anthropic({ + apiKey: voidConfig.anthropic.apikey, + dangerouslyAllowBrowser: true + }) + + // Combine system messages into a single string + const systemMessage = messages + .filter(msg => msg.role === 'system') + .map(msg => msg.content) + .join('\n') + + // Remove system messages and cast to Anthropic message type + const anthropicMessages = messages + .filter(msg => msg.role !== 'system') as LLMMessageAnthropic[] + + let did_abort = false + + const stream = anthropic.messages.stream({ + system: systemMessage, + messages: anthropicMessages, + model: voidConfig.anthropic.model, + max_tokens: parseMaxTokensStr(voidConfig.default.maxTokens)!, + }) + + // Handle streaming response + stream.on('text', (newText, fullText) => { + if (did_abort) return + onText(newText, fullText) + }) + + // Handle final message + stream.on('finalMessage', (response) => { + if (did_abort) return + const content = response.content + .map(c => c.type === 'text' ? c.text : '') + .join('\n') + onFinalMessage(content) + }) + + // Handle errors + stream.on('error', (error) => { + if (error instanceof Anthropic.APIError && error.status === 401) { + onError('Invalid API key.') + } else { + onError(error.message) + } + }) + + return { + abort: () => { + did_abort = true + stream.controller.abort() + } + } +} diff --git a/extensions/void/src/common/llm/providers/gemini.ts b/extensions/void/src/common/llm/providers/gemini.ts new file mode 100644 index 00000000..fec2ab65 --- /dev/null +++ b/extensions/void/src/common/llm/providers/gemini.ts @@ -0,0 +1,68 @@ +import { GoogleGenerativeAI, GoogleGenerativeAIFetchError } from '@google/generative-ai' +import { SendLLMMessageParams } from '../types' +import { parseMaxTokensStr } from '../utils' + +export const sendGeminiMsg = async ({ + messages, + onText, + onFinalMessage, + onError, + voidConfig, + abortRef +}: SendLLMMessageParams) => { + let didAbort = false + let fullText = '' + + abortRef.current = () => { + didAbort = true + } + + const genAI = new GoogleGenerativeAI(voidConfig.gemini.apikey) + const model = genAI.getGenerativeModel({ model: voidConfig.gemini.model }) + + // Get system messages and combine them + const systemMessage = messages + .filter(msg => msg.role === 'system') + .map(msg => msg.content) + .join('\n') + + // Convert messages to Gemini format + const geminiMessages = messages + .filter(msg => msg.role !== 'system') + .map(msg => ({ + parts: [{ text: msg.content }], + role: msg.role === 'assistant' ? 'model' : 'user' + })) + + try { + const response = await model.generateContentStream({ + contents: geminiMessages, + systemInstruction: systemMessage, + }) + + abortRef.current = () => { + didAbort = true + } + + for await (const chunk of response.stream) { + if (didAbort) return + const newText = chunk.text() + fullText += newText + onText(newText, fullText) + } + + onFinalMessage(fullText) + } catch (error) { + if (error instanceof GoogleGenerativeAIFetchError) { + if (error.status === 400) { + onError('Invalid API key.') + } else { + onError(`${error.name}:\n${error.message}`) + } + } else if (error instanceof Error) { + onError(error.toString()) + } else { + onError('Unknown error occurred') + } + } +} diff --git a/extensions/void/src/common/llm/providers/greptile.ts b/extensions/void/src/common/llm/providers/greptile.ts new file mode 100644 index 00000000..1f66d136 --- /dev/null +++ b/extensions/void/src/common/llm/providers/greptile.ts @@ -0,0 +1,98 @@ +import { SendLLMMessageParams } from '../types' + +// Response type for Greptile API +type GreptileResponse = { + type: 'message' | 'sources' | 'status' + message: string | { + filepath: string + linestart: number | null + lineend: number | null + } | '' +} + +// Sends a message to Greptile API and handles the streaming response +export const sendGreptileMsg = ({ + messages, + onText, + onFinalMessage, + onError, + voidConfig, + abortRef +}: SendLLMMessageParams) => { + let didAbort = false + let fullText = '' + + // Set up abort handler + abortRef.current = () => { + didAbort = true + } + + // Make API request to Greptile + fetch('https://api.greptile.com/v2/query', { + method: 'POST', + headers: { + "Authorization": `Bearer ${voidConfig.greptile.apikey}`, + "X-Github-Token": `${voidConfig.greptile.githubPAT}`, + "Content-Type": `application/json`, + }, + body: JSON.stringify({ + messages, + stream: true, + repositories: [voidConfig.greptile.repoinfo], + }), + }) + .then(async response => { + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + // Parse the streaming response into JSON array + const text = await response.text() + return JSON.parse(`[${text.trim().split('\n').join(',')}]`) as GreptileResponse[] + }) + .then(async responseArr => { + if (didAbort) return + + // Process each response chunk + for (const response of responseArr) { + if (didAbort) break + + switch (response.type) { + case 'message': + // Handle message chunks + fullText += response.message as string + onText(response.message as string, fullText) + break + + case 'sources': { + // Handle source reference chunks + const sourceInfo = response.message as { + filepath: string + linestart: number | null + lineend: number | null + } + const sourceText = `\nSource: ${sourceInfo.filepath}${sourceInfo.linestart + ? ` (lines ${sourceInfo.linestart}-${sourceInfo.lineend})` + : '' + }\n` + fullText += sourceText + onText(sourceText, fullText) + break + } + + case 'status': + // Handle completion status + if (!response.message) { + onFinalMessage(fullText) + } + break + } + } + }) + .catch(error => { + // Handle any errors that occur during the request + const errorMessage = error instanceof Error + ? error.message + : 'An unknown error occurred' + onError(errorMessage) + }) +} diff --git a/extensions/void/src/common/llm/providers/ollama.ts b/extensions/void/src/common/llm/providers/ollama.ts new file mode 100644 index 00000000..b9b548ce --- /dev/null +++ b/extensions/void/src/common/llm/providers/ollama.ts @@ -0,0 +1,113 @@ +import { Ollama } from 'ollama/browser' +import { SendLLMMessageParams } from '../types' +import { parseMaxTokensStr } from '../utils' + +/** + * Check if an Ollama model is installed + */ +async function checkModelExists(ollama: Ollama, modelName: string): Promise<{ + exists: boolean, + installedModels: string[] +}> { + const models = await ollama.list() + const installedModels = models.models.map(m => m.name.replace(/:latest$/, '')) + const exists = installedModels.some(m => m.startsWith(modelName)) + return { exists, installedModels } +} + +/** + * Build error message for when model is not found + */ +function buildModelNotFoundError(modelName: string, installedModels: string[]): string { + return [ + `The model "${modelName}" is not available locally.`, + `Please run 'ollama pull ${modelName}' to download it first`, + `or try selecting one from the installed models:`, + installedModels.join(', ') + ].join(' ') +} + +/** + * Implementation of Ollama chat functionality + */ +export const sendOllamaMsg = async ({ + messages, + onText, + onFinalMessage, + onError, + voidConfig, + abortRef +}: SendLLMMessageParams) => { + let didAbort = false + let fullText = "" + + // Set up abort handler + abortRef.current = () => { + didAbort = true + } + + try { + // Initialize Ollama client + const ollama = new Ollama({ + host: voidConfig.ollama.endpoint + }) + + // Check if model exists + const { exists, installedModels } = await checkModelExists( + ollama, + voidConfig.ollama.model + ) + + if (!exists) { + const errorMessage = buildModelNotFoundError( + voidConfig.ollama.model, + installedModels + ) + onText(errorMessage, errorMessage) + onFinalMessage(errorMessage) + return + } + + // Start streaming chat response + const stream = await ollama.chat({ + model: voidConfig.ollama.model, + messages, + stream: true, + options: { + num_predict: parseMaxTokensStr(voidConfig.default.maxTokens) + } + }) + + // Update abort handler + abortRef.current = () => { + didAbort = true + } + + // Handle streaming response + for await (const chunk of stream) { + if (didAbort) return + + const newText = chunk.message.content + fullText += newText + onText(newText, fullText) + } + + // Send final message + onFinalMessage(fullText) + + } catch (error) { + // Handle connection errors + if (error instanceof Error && error.message.includes('Failed to fetch')) { + const errorMessage = [ + 'Ollama service is not running.', + 'Please start the Ollama service and try again.' + ].join(' ') + onText(errorMessage, errorMessage) + onFinalMessage(errorMessage) + } + // Handle other errors + else if (error) { + onError(error.toString()) + } + } +} diff --git a/extensions/void/src/common/llm/providers/openai.ts b/extensions/void/src/common/llm/providers/openai.ts new file mode 100644 index 00000000..4bbdbb72 --- /dev/null +++ b/extensions/void/src/common/llm/providers/openai.ts @@ -0,0 +1,103 @@ +import OpenAI from 'openai' +import { SendLLMMessageParams } from '../types' +import { parseMaxTokensStr } from '../utils' + +export const sendOpenAIMsg = ({ + messages, + onText, + onFinalMessage, + onError, + voidConfig, + abortRef +}: SendLLMMessageParams) => { + let didAbort = false + let fullText = '' + + abortRef.current = () => { + didAbort = true + } + + let openai: OpenAI + let options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming + + const maxTokens = parseMaxTokensStr(voidConfig.default.maxTokens) + + // Configure OpenAI client based on API type + switch (voidConfig.default.whichApi) { + case 'openAI': + openai = new OpenAI({ + apiKey: voidConfig.openAI.apikey, + dangerouslyAllowBrowser: true + }) + options = { + model: voidConfig.openAI.model, + messages, + stream: true, + max_tokens: maxTokens + } + break + + case 'openRouter': + openai = new OpenAI({ + baseURL: "https://openrouter.ai/api/v1", + apiKey: voidConfig.openRouter.apikey, + dangerouslyAllowBrowser: true, + defaultHeaders: { + "HTTP-Referer": 'https://voideditor.com', + "X-Title": 'Void Editor', + } + }) + options = { + model: voidConfig.openRouter.model, + messages, + stream: true, + max_tokens: maxTokens + } + break + + case 'openAICompatible': + openai = new OpenAI({ + baseURL: voidConfig.openAICompatible.endpoint, + apiKey: voidConfig.openAICompatible.apikey, + dangerouslyAllowBrowser: true + }) + options = { + model: voidConfig.openAICompatible.model, + messages, + stream: true, + max_tokens: maxTokens + } + break + + default: + throw new Error(`Invalid whichApi: ${voidConfig.default.whichApi}`) + } + + openai.chat.completions + .create(options) + .then(async response => { + abortRef.current = () => { + didAbort = true + } + + for await (const chunk of response) { + if (didAbort) return + const newText = chunk.choices[0]?.delta?.content || '' + fullText += newText + onText(newText, fullText) + } + + onFinalMessage(fullText) + }) + .catch(error => { + if (error instanceof OpenAI.APIError) { + if (error.status === 401) { + onError('Invalid API key.') + } else { + onError(`${error.name}:\n${error.message}`) + } + } else { + onError(error) + } + }) +} diff --git a/extensions/void/src/common/llm/types.ts b/extensions/void/src/common/llm/types.ts new file mode 100644 index 00000000..8b9901b9 --- /dev/null +++ b/extensions/void/src/common/llm/types.ts @@ -0,0 +1,26 @@ +import { VoidConfig } from '../../webviews/common/contextForConfig' + +export type AbortRef = { current: (() => void) | null } + +export type OnText = (newText: string, fullText: string) => void + +export type OnFinalMessage = (input: string) => void + +export type LLMMessageAnthropic = { + role: 'user' | 'assistant', + content: string, +} + +export type LLMMessage = { + role: 'system' | 'user' | 'assistant', + content: string, +} + +export type SendLLMMessageParams = { + messages: LLMMessage[], + onText: OnText, + onFinalMessage: OnFinalMessage, + onError: (error: string) => void, + voidConfig: VoidConfig, + abortRef: AbortRef, +} diff --git a/extensions/void/src/common/llm/utils.ts b/extensions/void/src/common/llm/utils.ts new file mode 100644 index 00000000..66d0f4df --- /dev/null +++ b/extensions/void/src/common/llm/utils.ts @@ -0,0 +1,6 @@ +export const parseMaxTokensStr = (maxTokensStr: string) => { + let int = isNaN(Number(maxTokensStr)) ? undefined : parseInt(maxTokensStr) + if (Number.isNaN(int)) + return undefined + return int +} diff --git a/extensions/void/src/common/sendLLMMessage.ts b/extensions/void/src/common/sendLLMMessage.ts deleted file mode 100644 index 5a6e8f0f..00000000 --- a/extensions/void/src/common/sendLLMMessage.ts +++ /dev/null @@ -1,382 +0,0 @@ -import Anthropic from '@anthropic-ai/sdk'; -import OpenAI from 'openai'; -import { Ollama } from 'ollama/browser' -import { Content, GoogleGenerativeAI, GoogleGenerativeAIError, GoogleGenerativeAIFetchError } from '@google/generative-ai'; -import { VoidConfig } from '../webviews/common/contextForConfig' - -export type AbortRef = { current: (() => void) | null } - -export type OnText = (newText: string, fullText: string) => void - -export type OnFinalMessage = (input: string) => void - -export type LLMMessageAnthropic = { - role: 'user' | 'assistant', - content: string, -} - -export type LLMMessage = { - role: 'system' | 'user' | 'assistant', - content: string, -} - -type SendLLMMessageFnTypeInternal = (params: { - messages: LLMMessage[], - onText: OnText, - onFinalMessage: OnFinalMessage, - onError: (error: string) => void, - voidConfig: VoidConfig, - abortRef: AbortRef, -}) => void - -type SendLLMMessageFnTypeExternal = (params: { - messages: LLMMessage[], - onText: OnText, - onFinalMessage: (fullText: string) => void, - onError: (error: string) => void, - voidConfig: VoidConfig | null, - abortRef: AbortRef, -}) => void - -const parseMaxTokensStr = (maxTokensStr: string) => { - // parse the string but only if the full string is a valid number, eg parseInt('100abc') should return NaN - let int = isNaN(Number(maxTokensStr)) ? undefined : parseInt(maxTokensStr) - if (Number.isNaN(int)) - return undefined - return int -} - -// Anthropic -const sendAnthropicMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig }) => { - - const anthropic = new Anthropic({ apiKey: voidConfig.anthropic.apikey, dangerouslyAllowBrowser: true }); // defaults to process.env["ANTHROPIC_API_KEY"] - - // find system messages and concatenate them - const systemMessage = messages - .filter(msg => msg.role === 'system') - .map(msg => msg.content) - .join('\n'); - - // remove system messages for Anthropic - const anthropicMessages = messages.filter(msg => msg.role !== 'system') as LLMMessageAnthropic[] - - const stream = anthropic.messages.stream({ - system: systemMessage, - messages: anthropicMessages, - model: voidConfig.anthropic.model, - max_tokens: parseMaxTokensStr(voidConfig.default.maxTokens)!, // this might be undefined, but it will just throw an error for the user - }); - - let did_abort = false - - // when receive text - stream.on('text', (newText, fullText) => { - if (did_abort) return - onText(newText, fullText) - }) - - // when we get the final message on this stream (or when error/fail) - stream.on('finalMessage', (claude_response) => { - if (did_abort) return - // stringify the response's content - let content = claude_response.content.map(c => { if (c.type === 'text') { return c.text } }).join('\n'); - onFinalMessage(content) - }) - - 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('Invalid API key.') - } - else { - onError(error.message) - } - }) - - // if abort is called, onFinalMessage is NOT called, and no later onTexts are called either - const abort = () => { - did_abort = true - stream.controller.abort() // TODO need to test this to make sure it works, it might throw an error - } - - return { abort } -}; - -// Gemini -const sendGeminiMsg: SendLLMMessageFnTypeInternal = async ({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }) => { - - let didAbort = false - let fullText = '' - - abortRef.current = () => { - didAbort = true - } - - const genAI = new GoogleGenerativeAI(voidConfig.gemini.apikey); - const model = genAI.getGenerativeModel({ model: voidConfig.gemini.model }); - - // remove system messages that get sent to Gemini - // str of all system messages - let systemMessage = messages - .filter(msg => msg.role === 'system') - .map(msg => msg.content) - .join('\n'); - - // Convert messages to Gemini format - const geminiMessages: Content[] = messages - .filter(msg => msg.role !== 'system') - .map((msg, i) => ({ - parts: [{ text: msg.content }], - role: msg.role === 'assistant' ? 'model' : 'user' - })) - - model.generateContentStream({ contents: geminiMessages, systemInstruction: systemMessage, }) - .then(async response => { - abortRef.current = () => { - // response.stream.return(fullText) - didAbort = true; - } - for await (const chunk of response.stream) { - if (didAbort) return; - const newText = chunk.text(); - fullText += newText; - onText(newText, fullText); - } - onFinalMessage(fullText); - }) - .catch((error) => { - if (error instanceof GoogleGenerativeAIFetchError) { - if (error.status === 400) { - onError('Invalid API key.'); - } - else { - onError(`${error.name}:\n${error.message}`); - } - } - else { - onError(error); - } - }) -} - -// OpenAI, OpenRouter, OpenAICompatible -const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }) => { - - let didAbort = false - let fullText = '' - - // if abort is called, onFinalMessage is NOT called, and no later onTexts are called either - abortRef.current = () => { - didAbort = true; - }; - - let openai: OpenAI - let options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming - - let maxTokens = parseMaxTokensStr(voidConfig.default.maxTokens) - - if (voidConfig.default.whichApi === 'openAI') { - openai = new OpenAI({ apiKey: voidConfig.openAI.apikey, dangerouslyAllowBrowser: true }); - options = { model: voidConfig.openAI.model, messages: messages, stream: true, max_completion_tokens: maxTokens } - } - else if (voidConfig.default.whichApi === 'openRouter') { - openai = new OpenAI({ - baseURL: "https://openrouter.ai/api/v1", apiKey: voidConfig.openRouter.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. - }, - }); - options = { model: voidConfig.openRouter.model, messages: messages, stream: true, max_completion_tokens: maxTokens } - } - else if (voidConfig.default.whichApi === 'openAICompatible') { - openai = new OpenAI({ baseURL: voidConfig.openAICompatible.endpoint, apiKey: voidConfig.openAICompatible.apikey, dangerouslyAllowBrowser: true }) - options = { model: voidConfig.openAICompatible.model, messages: messages, stream: true, max_completion_tokens: maxTokens } - } - else { - console.error(`sendOpenAIMsg: invalid whichApi: ${voidConfig.default.whichApi}`) - throw new Error(`voidConfig.whichAPI was invalid: ${voidConfig.default.whichApi}`) - } - - openai.chat.completions - .create(options) - .then(async response => { - abortRef.current = () => { - // response.controller.abort() - didAbort = true; - } - // when receive text - for await (const chunk of response) { - if (didAbort) return; - const newText = chunk.choices[0]?.delta?.content || ''; - fullText += newText; - onText(newText, fullText); - } - onFinalMessage(fullText); - }) - // when error/fail - this catches errors of both .create() and .then(for await) - .catch(error => { - if (error instanceof OpenAI.APIError) { - if (error.status === 401) { - onError('Invalid API key.'); - } - else { - onError(`${error.name}:\n${error.message}`); - } - } - else { - onError(error); - } - }) - -}; - -// Ollama -export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }) => { - - let didAbort = false - let fullText = "" - - abortRef.current = () => { - didAbort = true; - }; - - const ollama = new Ollama({ host: voidConfig.ollama.endpoint }) - - // First check if model exists - ollama.list() - .then(async models => { - const installedModels = models.models.map(m => m.name.replace(/:latest$/, '')) - const modelExists = installedModels.some(m => m.startsWith(voidConfig.ollama.model)); - if (!modelExists) { - const errorMessage = `The model "${voidConfig.ollama.model}" is not available locally. Please run 'ollama pull ${voidConfig.ollama.model}' to download it first or - try selecting one from the Installed models: ${installedModels.join(', ')}`; - onText(errorMessage, errorMessage); - onFinalMessage(errorMessage); - return Promise.reject(); - } - - return ollama.chat({ - model: voidConfig.ollama.model, - messages: messages, - stream: true, - options: { num_predict: parseMaxTokensStr(voidConfig.default.maxTokens) } - }); - }) - .then(async stream => { - if (!stream) return; - - abortRef.current = () => { - didAbort = true - } - for await (const chunk of stream) { - if (didAbort) return; - const newText = chunk.message.content; - fullText += newText; - onText(newText, fullText); - } - onFinalMessage(fullText); - }) - .catch(error => { - // Check if the error is a connection error - if (error instanceof Error && error.message.includes('Failed to fetch')) { - const errorMessage = 'Ollama service is not running. Please start the Ollama service and try again.'; - onText(errorMessage, errorMessage); - onFinalMessage(errorMessage); - } else if (error) { - onError(error); - } - }); -}; - -// Greptile -// https://docs.greptile.com/api-reference/query -// https://docs.greptile.com/quickstart#sample-response-streamed - -const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }) => { - - let didAbort = false - let fullText = '' - - // if abort is called, onFinalMessage is NOT called, and no later onTexts are called either - abortRef.current = () => { - didAbort = true - } - - fetch('https://api.greptile.com/v2/query', { - method: 'POST', - headers: { - "Authorization": `Bearer ${voidConfig.greptile.apikey}`, - "X-Github-Token": `${voidConfig.greptile.githubPAT}`, - "Content-Type": `application/json`, - }, - body: JSON.stringify({ - messages, - stream: true, - repositories: [voidConfig.greptile.repoinfo], - }), - }) - // this is {message}\n{message}\n{message}...\n - .then(async response => { - const text = await response.text() - console.log('got greptile', text) - return JSON.parse(`[${text.trim().split('\n').join(',')}]`) - }) - // TODO make this actually stream, right now it just sends one message at the end - .then(async responseArr => { - if (didAbort) - return - - for (let response of responseArr) { - - const type: string = response['type'] - const message = response['message'] - - // when receive text - if (type === 'message') { - fullText += message - onText(message, fullText) - } - else if (type === 'sources') { - const { filepath, linestart, lineend } = message as { filepath: string, linestart: number | null, lineend: number | null } - fullText += filepath - onText(filepath, fullText) - } - // type: 'status' with an empty 'message' means last message - else if (type === 'status') { - if (!message) { - onFinalMessage(fullText) - } - } - } - - }) - .catch(e => { - onError(e) - }); - -} - -export const sendLLMMessage: SendLLMMessageFnTypeExternal = ({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }) => { - if (!voidConfig) return; - - // trim message content (Anthropic and other providers give an error if there is trailing whitespace) - messages = messages.map(m => ({ ...m, content: m.content.trim() })) - - switch (voidConfig.default.whichApi) { - case 'anthropic': - return sendAnthropicMsg({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }); - case 'openAI': - case 'openRouter': - case 'openAICompatible': - return sendOpenAIMsg({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }); - case 'gemini': - return sendGeminiMsg({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }); - case 'ollama': - return sendOllamaMsg({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }); - case 'greptile': - return sendGreptileMsg({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }); - default: - onError(`Error: whichApi was ${voidConfig.default.whichApi}, which is not recognized!`) - } -} diff --git a/extensions/void/src/extension/applyDiffLazily.ts b/extensions/void/src/extension/applyDiffLazily.ts index 9b350082..fac0608b 100644 --- a/extensions/void/src/extension/applyDiffLazily.ts +++ b/extensions/void/src/extension/applyDiffLazily.ts @@ -1,5 +1,7 @@ import * as vscode from 'vscode'; -import { AbortRef, sendLLMMessage } from '../common/sendLLMMessage'; + +import { sendLLMMessage } from '../common/llm'; +import { AbortRef } from '../common/llm/types'; import { DiffArea } from '../common/shared_types'; import { writeFileWithDiffInstructions, searchDiffChunkInstructions } from '../common/systemPrompts'; import { VoidConfig } from '../webviews/common/contextForConfig'; diff --git a/extensions/void/src/extension/ctrlK.ts b/extensions/void/src/extension/ctrlK.ts index 63aaf200..8df34cdb 100644 --- a/extensions/void/src/extension/ctrlK.ts +++ b/extensions/void/src/extension/ctrlK.ts @@ -1,5 +1,6 @@ import * as vscode from 'vscode'; -import { AbortRef, OnFinalMessage, OnText, sendLLMMessage } from "../common/sendLLMMessage" +import { sendLLMMessage } from "../common/llm" +import { AbortRef, OnFinalMessage, OnText } from '../common/llm/types' import { VoidConfig } from '../webviews/common/contextForConfig'; import { searchDiffChunkInstructions, writeFileWithDiffInstructions } from '../common/systemPrompts'; import { throttle } from 'lodash'; @@ -97,4 +98,4 @@ Complete the following: -export { applyCtrlK } \ No newline at end of file +export { applyCtrlK } diff --git a/extensions/void/src/extension/extension.ts b/extensions/void/src/extension/extension.ts index cc7cbed6..de801e6b 100644 --- a/extensions/void/src/extension/extension.ts +++ b/extensions/void/src/extension/extension.ts @@ -1,7 +1,7 @@ import * as vscode from 'vscode'; import { v4 as uuidv4 } from 'uuid' -import { AbortRef } from '../common/sendLLMMessage'; +import { AbortRef } from '../common/llm/types'; import { MessageToSidebar, MessageFromSidebar, DiffArea, ChatThreads } from '../common/shared_types'; import { getVoidConfigFromPartial } from '../webviews/common/contextForConfig'; import { applyDiffLazily } from './applyDiffLazily'; diff --git a/extensions/void/src/webviews/sidebar/SidebarChat.tsx b/extensions/void/src/webviews/sidebar/SidebarChat.tsx index 038fedf7..cac3e295 100644 --- a/extensions/void/src/webviews/sidebar/SidebarChat.tsx +++ b/extensions/void/src/webviews/sidebar/SidebarChat.tsx @@ -8,7 +8,7 @@ import { File, ChatMessage, CodeSelection } from "../../common/shared_types"; import * as vscode from 'vscode' import { awaitVSCodeResponse, getVSCodeAPI, onMessageFromVSCode, useOnVSCodeMessage } from "../common/getVscodeApi"; import { useThreads } from "../common/contextForThreads"; -import { sendLLMMessage } from "../../common/sendLLMMessage"; +import { sendLLMMessage } from "../../common/llm"; import { useVoidConfig } from "../common/contextForConfig"; import { captureEvent } from "../common/posthog"; import { generateDiffInstructions } from "../../common/systemPrompts";