From aa6b5c6b6c6eaec75134ff8dbb8e19b6a14ac504 Mon Sep 17 00:00:00 2001 From: pythons Date: Mon, 11 Nov 2024 15:10:32 +0800 Subject: [PATCH 1/5] refactor: modularize LLM providers and types - Split monolithic sendLLMMessage.ts into separate provider modules - Create dedicated files for each LLM provider (anthropic, gemini, openai, ollama, greptile) - Move types into separate types.ts file - Update import paths across affected files - No functional changes, purely architectural improvement This refactoring improves code organization and maintainability by: - Separating concerns for each LLM provider - Making the codebase more modular - Improving type management --- extensions/void/src/common/llm/index.ts | 49 +++ .../src/common/llm/providers/anthropic.ts | 66 +++ .../void/src/common/llm/providers/gemini.ts | 68 ++++ .../void/src/common/llm/providers/greptile.ts | 98 +++++ .../void/src/common/llm/providers/ollama.ts | 113 ++++++ .../void/src/common/llm/providers/openai.ts | 103 +++++ extensions/void/src/common/llm/types.ts | 26 ++ extensions/void/src/common/llm/utils.ts | 6 + extensions/void/src/common/sendLLMMessage.ts | 382 ------------------ .../void/src/extension/applyDiffLazily.ts | 4 +- extensions/void/src/extension/ctrlK.ts | 5 +- extensions/void/src/extension/extension.ts | 2 +- .../void/src/webviews/sidebar/SidebarChat.tsx | 2 +- 13 files changed, 537 insertions(+), 387 deletions(-) create mode 100644 extensions/void/src/common/llm/index.ts create mode 100644 extensions/void/src/common/llm/providers/anthropic.ts create mode 100644 extensions/void/src/common/llm/providers/gemini.ts create mode 100644 extensions/void/src/common/llm/providers/greptile.ts create mode 100644 extensions/void/src/common/llm/providers/ollama.ts create mode 100644 extensions/void/src/common/llm/providers/openai.ts create mode 100644 extensions/void/src/common/llm/types.ts create mode 100644 extensions/void/src/common/llm/utils.ts delete mode 100644 extensions/void/src/common/sendLLMMessage.ts 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"; From 3a1955901e702d94dfcd886fcad918e1615b9a30 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Wed, 4 Dec 2024 14:46:36 -0800 Subject: [PATCH 2/5] move natively --- .../createJsProgramGraph.ts | 0 .../findFunctions.ts | 0 .../void/src/extension/AutcompleteProvider.ts | 471 --------- .../react/src/sendLLMMessage/_types.tsx | 12 + .../react/src/sendLLMMessage/anthropic.tsx | 56 + .../react/src/sendLLMMessage/gemini.tsx | 46 + .../react/src/sendLLMMessage/greptile.tsx | 62 ++ .../react/src/sendLLMMessage/ollama.tsx | 37 + .../react/src/sendLLMMessage/openai.tsx | 61 ++ .../src/sendLLMMessage/sendLLMMessage.tsx | 96 ++ .../browser/react/src/util/sendLLMMessage.tsx | 986 ------------------ .../contrib/void/browser/registerConfig.ts | 9 + 12 files changed, 379 insertions(+), 1457 deletions(-) rename extensions/void/src/common/{LangaugeServer => LangaugeServerTest}/createJsProgramGraph.ts (100%) rename extensions/void/src/common/{LangaugeServer => LangaugeServerTest}/findFunctions.ts (100%) delete mode 100644 extensions/void/src/extension/AutcompleteProvider.ts create mode 100644 src/vs/workbench/contrib/void/browser/react/src/sendLLMMessage/_types.tsx create mode 100644 src/vs/workbench/contrib/void/browser/react/src/sendLLMMessage/anthropic.tsx create mode 100644 src/vs/workbench/contrib/void/browser/react/src/sendLLMMessage/gemini.tsx create mode 100644 src/vs/workbench/contrib/void/browser/react/src/sendLLMMessage/greptile.tsx create mode 100644 src/vs/workbench/contrib/void/browser/react/src/sendLLMMessage/ollama.tsx create mode 100644 src/vs/workbench/contrib/void/browser/react/src/sendLLMMessage/openai.tsx create mode 100644 src/vs/workbench/contrib/void/browser/react/src/sendLLMMessage/sendLLMMessage.tsx delete mode 100644 src/vs/workbench/contrib/void/browser/react/src/util/sendLLMMessage.tsx diff --git a/extensions/void/src/common/LangaugeServer/createJsProgramGraph.ts b/extensions/void/src/common/LangaugeServerTest/createJsProgramGraph.ts similarity index 100% rename from extensions/void/src/common/LangaugeServer/createJsProgramGraph.ts rename to extensions/void/src/common/LangaugeServerTest/createJsProgramGraph.ts diff --git a/extensions/void/src/common/LangaugeServer/findFunctions.ts b/extensions/void/src/common/LangaugeServerTest/findFunctions.ts similarity index 100% rename from extensions/void/src/common/LangaugeServer/findFunctions.ts rename to extensions/void/src/common/LangaugeServerTest/findFunctions.ts diff --git a/extensions/void/src/extension/AutcompleteProvider.ts b/extensions/void/src/extension/AutcompleteProvider.ts deleted file mode 100644 index 4beb7a67..00000000 --- a/extensions/void/src/extension/AutcompleteProvider.ts +++ /dev/null @@ -1,471 +0,0 @@ -import * as vscode from 'vscode'; -import { AbortRef, LLMMessage, sendLLMMessage } from '../common/sendLLMMessage'; -import { getVoidConfigFromPartial, VoidConfig } from '../webviews/common/contextForConfig'; -import { LRUCache } from 'lru-cache'; - - - -// The extension this was called from is here - https://github.com/voideditor/void/blob/autocomplete/extensions/void/src/extension/extension.ts - - -/* -A summary of autotab: - -Postprocessing --one common problem for all models is outputting unbalanced parentheses -we solve this by trimming all extra closing parentheses from the generated string -in future, should make sure parentheses are always balanced - --another problem is completing the middle of a string, eg. "const [x, CURSOR] = useState()" -we complete up to first matchup character -but should instead complete the whole line / block (difficult because of parenthesis accuracy) - --too much info is bad. usually we want to show the user 1 line, and have a preloaded response afterwards -this should happen automatically with caching system -should break preloaded responses into \n\n chunks - -Preprocessing -- we don't generate if cursor is at end / beginning of a line (no spaces) -- we generate 1 line if there is text to the right of cursor -- we generate 1 line if variable declaration -- (in many cases want to show 1 line but generate multiple) - -State -- cache based on prefix (and do some trimming first) -- when press tab on one line, should have an immediate followup response -to do this, show autocompletes before they're fully finished -- [todo] remove each autotab when accepted -- [todo] treat windows \r\n separately from \n -!- [todo] provide type information - -Details --generated results are trimmed up to 1 leading/trailing space --prefixes are cached up to 1 trailing newline -- -*/ - - - - - -type AutocompletionStatus = 'pending' | 'finished' | 'error'; -type Autocompletion = { - id: number, - prefix: string, - suffix: string, - startTime: number, - endTime: number | undefined, - abortRef: AbortRef, - status: AutocompletionStatus, - llmPromise: Promise | undefined, - result: string, -} - -const DEBOUNCE_TIME = 500 -const TIMEOUT_TIME = 60000 -const MAX_CACHE_SIZE = 20 -const MAX_PENDING_REQUESTS = 2 - -// postprocesses the result -const postprocessResult = (result: string) => { - - console.log('result: ', JSON.stringify(result)) - - // trim all whitespace except for a single leading/trailing space - const hasLeadingSpace = result.startsWith(' '); - const hasTrailingSpace = result.endsWith(' '); - return (hasLeadingSpace ? ' ' : '') - + result.trim() - + (hasTrailingSpace ? ' ' : ''); - -} - -const extractCodeFromResult = (result: string) => { - - // extract the code between triple backticks - const parts = result.split(/```(?:\s*\w+)?\n?/); - - // if there is no ``` then return the raw result - if (parts.length === 1) { - return result; - } - - // else return the code between the triple backticks - return parts[1] - -} - -// trims the end of the prefix to improve cache hit rate -const trimPrefix = (prefix: string) => { - const trimmedPrefix = prefix.trimEnd() - const trailingEnd = prefix.substring(trimmedPrefix.length) - - // keep only a single trailing newline - if (trailingEnd.includes('\n')) { - return trimmedPrefix + '\n' - } - - // else ignore all spaces and return the trimmed prefix - return trimmedPrefix -} - -function getStringUpToUnbalancedParenthesis(s: string, prefixToTheLeft: string): string { - - const pairs: Record = { ')': '(', '}': '{', ']': '[' }; - - // todo find first open bracket in prefix and get all brackets beyond it in prefix - // get all bracets in prefix - let stack: string[] = [] - const firstOpenIdx = prefixToTheLeft.search(/[[({]/); - if (firstOpenIdx !== -1) stack = prefixToTheLeft.slice(firstOpenIdx).split('').filter(c => '()[]{}'.includes(c)) - - // Iterate through each character - for (let i = 0; i < s.length; i++) { - const char = s[i]; - - if (char === '(' || char === '{' || char === '[') { stack.push(char); } - else if (char === ')' || char === '}' || char === ']') { - if (stack.length === 0 || stack.pop() !== pairs[char]) { return s.substring(0, i); } - } - } - return s; -} - -// finds the text in the autocompletion to display, assuming the prefix is already matched -// example: -// originalPrefix = abcd -// generatedMiddle = efgh -// originalSuffix = ijkl -// the user has typed "ef" so prefix = abcdef -// we want to return the rest of the generatedMiddle, which is "gh" -const toInlineCompletion = ({ prefix, suffix, autocompletion, position }: { prefix: string, suffix: string, autocompletion: Autocompletion, position: vscode.Position }): vscode.InlineCompletionItem => { - const originalPrefix = autocompletion.prefix - const generatedMiddle = autocompletion.result - - const trimmedOriginalPrefix = trimPrefix(originalPrefix) - const trimmedCurrentPrefix = trimPrefix(prefix) - - const suffixLines = suffix.split('\n') - const prefixLines = trimmedCurrentPrefix.split('\n') - const suffixToTheRightOfCursor = suffixLines[0].trim() - const prefixToTheLeftOfCursor = prefixLines[prefixLines.length - 1].trim() - - const generatedLines = generatedMiddle.split('\n') - - // compute startIdx - let startIdx = trimmedCurrentPrefix.length - trimmedOriginalPrefix.length - if (startIdx < 0) { - return new vscode.InlineCompletionItem('') - } - - // compute endIdx - // hacks to get the suffix to render properly with lower quality models - // if the generated text matches with the suffix on the current line, stop - let endIdx: number | undefined = generatedMiddle.length // exclusive bounds - - if (suffixToTheRightOfCursor !== '') { // completing in the middle of a line - console.log('1') - // complete until there is a match - const matchIndex = generatedMiddle.lastIndexOf(suffixToTheRightOfCursor[0]) - if (matchIndex > 0) { endIdx = matchIndex } - } - - if (prefixToTheLeftOfCursor !== '') { // completing the end of a line - console.log('2') - // show a single line - const newlineIdx = generatedMiddle.indexOf('\n') - if (newlineIdx > -1) { endIdx = newlineIdx } - } - - // // if a generated line matches with a suffix line, stop - // if (suffixLines.length > 1) { - // console.log('3') - // const lines = [] - // for (const generatedLine of generatedLines) { - // if (suffixLines.slice(0, 10).some(suffixLine => - // generatedLine.trim() !== '' && suffixLine.trim() !== '' - // && generatedLine.trim().startsWith(suffixLine.trim()) - // )) break; - // lines.push(generatedLine) - // } - // endIdx = lines.join('\n').length // this is hacky, remove or refactor in future - // } - - let completionStr = generatedMiddle.slice(startIdx, endIdx) - - // filter out unbalanced parentheses - console.log('completionStrBeforeParens: ', JSON.stringify(completionStr)) - completionStr = getStringUpToUnbalancedParenthesis(completionStr, prefixLines.slice(-2).join('\n')) - - console.log('originalCompletionStr: ', JSON.stringify(generatedMiddle.slice(startIdx))) - console.log('finalCompletionStr: ', JSON.stringify(completionStr)) - - return new vscode.InlineCompletionItem(completionStr, new vscode.Range(position, position)) - -} - -// returns whether this autocompletion is in the cache -const doesPrefixMatchAutocompletion = ({ prefix, autocompletion }: { prefix: string, autocompletion: Autocompletion }): boolean => { - - const originalPrefix = autocompletion.prefix - const generatedMiddle = autocompletion.result - const originalPrefixTrimmed = trimPrefix(originalPrefix) - const currentPrefixTrimmed = trimPrefix(prefix) - - if (currentPrefixTrimmed.length < originalPrefixTrimmed.length) { - return false - } - - const isMatch = (originalPrefixTrimmed + generatedMiddle).startsWith(currentPrefixTrimmed) - return isMatch - -} - -const getCompletionOptions = ({ prefix, suffix }: { prefix: string, suffix: string }) => { - - const prefixLines = prefix.split('\n') - const suffixLines = suffix.split('\n') - - const prefixToLeftOfCursor = prefixLines.slice(-1)[0] ?? '' - const suffixToRightOfCursor = suffixLines[0] - - // default parameters - let shouldGenerate = true - let stopTokens: string[] = ['\n\n', '\r\n\r\n'] - - // specific cases - if (suffixToRightOfCursor.trim() !== '') { // typing between something - stopTokens = ['\n', '\r\n'] - } - - // if (prefixToLeftOfCursor.trim() === '' && suffixToRightOfCursor.trim() === '') { // at an empty line - // stopTokens = ['\n\n', '\r\n\r\n'] - // } - - if (prefixToLeftOfCursor === '' || suffixToRightOfCursor === '') { // at beginning or end of line - shouldGenerate = false - } - - console.log('shouldGenerate:', shouldGenerate, stopTokens) - - return { shouldGenerate, stopTokens } - -} - -export class AutocompleteProvider implements vscode.InlineCompletionItemProvider { - - private _extensionContext: vscode.ExtensionContext; - - private _autocompletionId: number = 0; - private _autocompletionsOfDocument: { [docUriStr: string]: LRUCache } = {} - - private _lastCompletionTime = 0 - private _lastPrefix: string = '' - - constructor(context: vscode.ExtensionContext) { - this._extensionContext = context - } - - // used internally by vscode - // fires after every keystroke and returns the completion to show - async provideInlineCompletionItems( - document: vscode.TextDocument, - position: vscode.Position, - context: vscode.InlineCompletionContext, - token: vscode.CancellationToken, - ): Promise { - - const disabled = false - if (disabled) { return []; } - - const docUriStr = document.uri.toString() - - - const fullText = document.getText(); - const cursorOffset = document.offsetAt(position); - const prefix = fullText.substring(0, cursorOffset) - const suffix = fullText.substring(cursorOffset) - const voidConfig = getVoidConfigFromPartial(this._extensionContext.globalState.get('partialVoidConfig') ?? {}) - - // initialize cache and other variables - // note that whenever an autocompletion is rejected, it is removed from cache - if (!this._autocompletionsOfDocument[docUriStr]) { - this._autocompletionsOfDocument[docUriStr] = new LRUCache({ - max: MAX_CACHE_SIZE, - dispose: (autocompletion) => { - autocompletion.abortRef.current() - } - }) - } - this._lastPrefix = prefix - - // get all pending autocompletions - let __c = 0 - this._autocompletionsOfDocument[docUriStr].forEach(a => { if (a.status === 'pending') __c += 1 }) - console.log('pending: ' + __c) - - // get autocompletion from cache - let cachedAutocompletion: Autocompletion | undefined = undefined - for (const autocompletion of this._autocompletionsOfDocument[docUriStr].values()) { - // if the user's change matches up with the generated text - if (doesPrefixMatchAutocompletion({ prefix, autocompletion })) { - cachedAutocompletion = autocompletion - break - } - } - - // if there is a cached autocompletion, return it - if (cachedAutocompletion) { - - if (cachedAutocompletion.status === 'finished') { - console.log('A1') - - const inlineCompletion = toInlineCompletion({ autocompletion: cachedAutocompletion, prefix, suffix, position }) - return [inlineCompletion] - - } else if (cachedAutocompletion.status === 'pending') { - console.log('A2') - - try { - await cachedAutocompletion.llmPromise; - console.log('id: ' + cachedAutocompletion.id) - const inlineCompletion = toInlineCompletion({ autocompletion: cachedAutocompletion, prefix, suffix, position }) - return [inlineCompletion] - - } catch (e) { - this._autocompletionsOfDocument[docUriStr].delete(cachedAutocompletion.id) - console.error('Error creating autocompletion (1): ' + e) - } - - } else if (cachedAutocompletion.status === 'error') { - console.log('A3') - } - - return [] - } - - // else if no more typing happens, then go forwards with the request - // wait DEBOUNCE_TIME for the user to stop typing - const thisTime = Date.now() - this._lastCompletionTime = thisTime - const didTypingHappenDuringDebounce = await new Promise((resolve, reject) => - setTimeout(() => { - if (this._lastCompletionTime === thisTime) { - resolve(false) - } else { - resolve(true) - } - }, DEBOUNCE_TIME) - ) - - // if more typing happened, then do not go forwards with the request - if (didTypingHappenDuringDebounce) { - return [] - } - - console.log('B') - - // if there are too many pending requests, cancel the oldest one - let numPending = 0 - let oldestPending: Autocompletion | undefined = undefined - for (const autocompletion of this._autocompletionsOfDocument[docUriStr].values()) { - if (autocompletion.status === 'pending') { - numPending += 1 - if (oldestPending === undefined) { - oldestPending = autocompletion - } - if (numPending >= MAX_PENDING_REQUESTS) { - // cancel the oldest pending request and remove it from cache - this._autocompletionsOfDocument[docUriStr].delete(oldestPending.id) - break - } - } - } - - const { shouldGenerate, stopTokens } = getCompletionOptions({ prefix, suffix }) - - if (!shouldGenerate) return [] - - // create a new autocompletion and add it to cache - const newAutocompletion: Autocompletion = { - id: this._autocompletionId++, - prefix: prefix, - suffix: suffix, - startTime: Date.now(), - endTime: undefined, - abortRef: { current: () => { } }, - status: 'pending', - llmPromise: undefined, - result: '', - } - - // set parameters of `newAutocompletion` appropriately - newAutocompletion.llmPromise = new Promise((resolve, reject) => { - - sendLLMMessage({ - mode: 'fim', - fimInfo: { prefix, suffix }, - options: { stopTokens }, - onText: async (tokenStr, completionStr) => { - - newAutocompletion.result = completionStr - - // if generation doesn't match the prefix for the first few tokens generated, reject it - if (!doesPrefixMatchAutocompletion({ prefix: this._lastPrefix, autocompletion: newAutocompletion })) { - reject('LLM response did not match user\'s text.') - } - }, - onFinalMessage: (finalMessage) => { - - // newAutocompletion.prefix = prefix - // newAutocompletion.suffix = suffix - // newAutocompletion.startTime = Date.now() - newAutocompletion.endTime = Date.now() - // newAutocompletion.abortRef = { current: () => { } } - newAutocompletion.status = 'finished' - // newAutocompletion.promise = undefined - newAutocompletion.result = postprocessResult(extractCodeFromResult(finalMessage)) - - resolve(newAutocompletion.result) - - }, - onError: (e) => { - newAutocompletion.endTime = Date.now() - newAutocompletion.status = 'error' - reject(e) - }, - voidConfig, - abortRef: newAutocompletion.abortRef, - }) - - // if the request hasnt resolved in TIMEOUT_TIME seconds, reject it - setTimeout(() => { - if (newAutocompletion.status === 'pending') { - reject('Timeout receiving message to LLM.') - } - }, TIMEOUT_TIME) - - - }) - - // add autocompletion to cache - this._autocompletionsOfDocument[docUriStr].set(newAutocompletion.id, newAutocompletion) - - // show autocompletion - try { - await newAutocompletion.llmPromise - console.log('id: ' + newAutocompletion.id) - - const inlineCompletion = toInlineCompletion({ autocompletion: newAutocompletion, prefix, suffix, position }) - return [inlineCompletion] - - } catch (e) { - this._autocompletionsOfDocument[docUriStr].delete(newAutocompletion.id) - console.error('Error creating autocompletion (2): ' + e) - return [] - } - - } - - -} diff --git a/src/vs/workbench/contrib/void/browser/react/src/sendLLMMessage/_types.tsx b/src/vs/workbench/contrib/void/browser/react/src/sendLLMMessage/_types.tsx new file mode 100644 index 00000000..6076cdac --- /dev/null +++ b/src/vs/workbench/contrib/void/browser/react/src/sendLLMMessage/_types.tsx @@ -0,0 +1,12 @@ +import { LLMMessage, OnError, OnFinalMessage, OnText } from '../../../../../../../platform/void/common/llmMessageTypes.js'; +import { VoidConfig } from '../../../registerConfig.js'; + +export type SendLLMMessageFnTypeInternal = (params: { + messages: LLMMessage[]; + onText: OnText; + onFinalMessage: OnFinalMessage; + onError: OnError; + voidConfig: VoidConfig; + + _setAborter: (aborter: () => void) => void; +}) => void diff --git a/src/vs/workbench/contrib/void/browser/react/src/sendLLMMessage/anthropic.tsx b/src/vs/workbench/contrib/void/browser/react/src/sendLLMMessage/anthropic.tsx new file mode 100644 index 00000000..4879d2c9 --- /dev/null +++ b/src/vs/workbench/contrib/void/browser/react/src/sendLLMMessage/anthropic.tsx @@ -0,0 +1,56 @@ +import Anthropic from '@anthropic-ai/sdk'; +import { SendLLMMessageFnTypeInternal } from './_types.js'; +import { parseMaxTokensStr } from '../../../registerConfig.js'; + +// Anthropic +type LLMMessageAnthropic = { + role: 'user' | 'assistant'; + content: string; +} +export const sendAnthropicMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter }) => { + + 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 + }); + + + // when receive text + stream.on('text', (newText, fullText) => { + onText({ newText, fullText }) + }) + + // when we get the final message on this stream (or when error/fail) + stream.on('finalMessage', (claude_response) => { + // stringify the response's content + const content = claude_response.content.map(c => c.type === 'text' ? c.text : c.type).join('\n'); + onFinalMessage({ fullText: 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({ error: 'Invalid API key.' }) + } + else { + onError({ error }) + } + }) + + // TODO need to test this to make sure it works, it might throw an error + _setAborter(() => stream.controller.abort()) + +}; diff --git a/src/vs/workbench/contrib/void/browser/react/src/sendLLMMessage/gemini.tsx b/src/vs/workbench/contrib/void/browser/react/src/sendLLMMessage/gemini.tsx new file mode 100644 index 00000000..3f350fee --- /dev/null +++ b/src/vs/workbench/contrib/void/browser/react/src/sendLLMMessage/gemini.tsx @@ -0,0 +1,46 @@ +import { Content, GoogleGenerativeAI, GoogleGenerativeAIFetchError } from '@google/generative-ai'; +import { SendLLMMessageFnTypeInternal } from './_types.js'; + +// Gemini +export const sendGeminiMsg: SendLLMMessageFnTypeInternal = async ({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter }) => { + + let fullText = '' + + 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 + const 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 => { + _setAborter(() => response.stream.return(fullText)) + + for await (const chunk of response.stream) { + const newText = chunk.text(); + fullText += newText; + onText({ newText, fullText }); + } + onFinalMessage({ fullText }); + }) + .catch((error) => { + if (error instanceof GoogleGenerativeAIFetchError && error.status === 400) { + onError({ error: 'Invalid API key.' }); + } + else { + onError({ error }); + } + }) +} diff --git a/src/vs/workbench/contrib/void/browser/react/src/sendLLMMessage/greptile.tsx b/src/vs/workbench/contrib/void/browser/react/src/sendLLMMessage/greptile.tsx new file mode 100644 index 00000000..81daada3 --- /dev/null +++ b/src/vs/workbench/contrib/void/browser/react/src/sendLLMMessage/greptile.tsx @@ -0,0 +1,62 @@ + +// Greptile +// https://docs.greptile.com/api-reference/query +// https://docs.greptile.com/quickstart#sample-response-streamed + +import { SendLLMMessageFnTypeInternal } from './_types.js'; + +export const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter }) => { + + let fullText = '' + + 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 + // TODO add _setAborter() when add streaming + .then(async responseArr => { + + for (const response of responseArr) { + const type: string = response['type'] + const message = response['message'] + + // when receive text + if (type === 'message') { + fullText += message + onText({ newText: message, fullText }) + } + else if (type === 'sources') { + const { filepath, linestart: _, lineend: _2 } = message as { filepath: string; linestart: number | null; lineend: number | null } + fullText += filepath + onText({ newText: filepath, fullText }) + } + // type: 'status' with an empty 'message' means last message + else if (type === 'status') { + if (!message) { + onFinalMessage({ fullText }) + } + } + } + + }) + .catch(error => { + onError({ error }) + }); + +} diff --git a/src/vs/workbench/contrib/void/browser/react/src/sendLLMMessage/ollama.tsx b/src/vs/workbench/contrib/void/browser/react/src/sendLLMMessage/ollama.tsx new file mode 100644 index 00000000..95f10a2e --- /dev/null +++ b/src/vs/workbench/contrib/void/browser/react/src/sendLLMMessage/ollama.tsx @@ -0,0 +1,37 @@ +import { Ollama } from 'ollama/browser'; +import { parseMaxTokensStr } from '../../../registerConfig.js'; +import { SendLLMMessageFnTypeInternal } from './_types.js'; + +// Ollama +export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter }) => { + + let fullText = '' + + const ollama = new Ollama({ host: voidConfig.ollama.endpoint }) + + ollama.chat({ + model: voidConfig.ollama.model, + messages: messages, + stream: true, + options: { num_predict: parseMaxTokensStr(voidConfig.default.maxTokens) } // this is max_tokens + }) + .then(async stream => { + _setAborter(() => stream.abort()) + // iterate through the stream + for await (const chunk of stream) { + const newText = chunk.message.content; + fullText += newText; + onText({ newText, fullText }); + } + onFinalMessage({ fullText }); + + }) + // when error/fail + .catch(error => { + onError({ error }) + }) + +}; + + + diff --git a/src/vs/workbench/contrib/void/browser/react/src/sendLLMMessage/openai.tsx b/src/vs/workbench/contrib/void/browser/react/src/sendLLMMessage/openai.tsx new file mode 100644 index 00000000..545787d1 --- /dev/null +++ b/src/vs/workbench/contrib/void/browser/react/src/sendLLMMessage/openai.tsx @@ -0,0 +1,61 @@ +import OpenAI from 'openai'; +import { parseMaxTokensStr } from '../../../registerConfig.js'; +import { SendLLMMessageFnTypeInternal } from './_types.js'; + + +// OpenAI, OpenRouter, OpenAICompatible +export const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter }) => { + + let fullText = '' + + let openai: OpenAI + let options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming + + const 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 => { + _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 }); + }) + // when error/fail - this catches errors of both .create() and .then(for await) + .catch(error => { + if (error instanceof OpenAI.APIError && error.status === 401) { + onError({ error: 'Invalid API key.' }); + } + else { + onError({ error }); + } + }) + +}; diff --git a/src/vs/workbench/contrib/void/browser/react/src/sendLLMMessage/sendLLMMessage.tsx b/src/vs/workbench/contrib/void/browser/react/src/sendLLMMessage/sendLLMMessage.tsx new file mode 100644 index 00000000..ab2c958e --- /dev/null +++ b/src/vs/workbench/contrib/void/browser/react/src/sendLLMMessage/sendLLMMessage.tsx @@ -0,0 +1,96 @@ +import { posthog } from 'posthog-js' +import type { OnText, OnError, OnFinalMessage, SendLLMMMessageParams, } from '../../../../../../../platform/void/common/llmMessageTypes.js'; +import { sendAnthropicMsg } from './anthropic.js'; +import { sendGeminiMsg } from './gemini.js'; +import { sendGreptileMsg } from './greptile.js'; +import { sendOllamaMsg } from './ollama.js'; +import { sendOpenAIMsg } from './openai.js'; + + +export const sendLLMMessage = ({ messages, onText: onText_, onFinalMessage: onFinalMessage_, onError: onError_, abortRef: abortRef_, voidConfig, logging: { loggingName }}: SendLLMMMessageParams) => { + 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() })) + + // only captures number of messages and message "shape", no actual code, instructions, prompts, etc + const captureChatEvent = (eventId: string, extras?: object) => { + posthog.capture(eventId, { + whichApi: voidConfig.default['whichApi'], + numMessages: messages?.length, + messagesShape: messages?.map(msg => ({ role: msg.role, length: msg.content.length })), + version: '2024-11-14', + ...extras, + }) + } + const submit_time = new Date() + + let _fullTextSoFar = '' + let _aborter: (() => void) | null = null + let _setAborter = (fn: () => void) => { _aborter = fn } + let _didAbort = false + + const onText: OnText = ({ newText, fullText }) => { + if (_didAbort) return + onText_({ newText, fullText }) + _fullTextSoFar = fullText + } + + const onFinalMessage: OnFinalMessage = ({ fullText }) => { + if (_didAbort) return + captureChatEvent(`${loggingName} - Received Full Message`, { messageLength: fullText.length, duration: new Date().getMilliseconds() - submit_time.getMilliseconds() }) + onFinalMessage_({ fullText }) + } + + const onError: OnError = ({ error }) => { + console.error('sendLLMMessage onError:', error) + if (_didAbort) return + captureChatEvent(`${loggingName} - Error`, { error }) + onError_({ error }) + } + + const onAbort = () => { + captureChatEvent(`${loggingName} - Abort`, { messageLengthSoFar: _fullTextSoFar.length }) + _aborter?.() + _didAbort = true + } + abortRef_.current = onAbort + + captureChatEvent(`${loggingName} - Sending Message`, { messageLength: messages[messages.length - 1]?.content.length }) + + try { + switch (voidConfig.default.whichApi) { + case 'anthropic': + sendAnthropicMsg({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter, }); + break; + case 'openAI': + case 'openRouter': + case 'openAICompatible': + sendOpenAIMsg({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter, }); + break; + case 'gemini': + sendGeminiMsg({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter, }); + break; + case 'ollama': + sendOllamaMsg({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter, }); + break; + case 'greptile': + sendGreptileMsg({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter, }); + break; + default: + onError({ error: `Error: whichApi was "${voidConfig.default.whichApi}", which is not recognized!` }) + break; + } + } + + catch (error) { + if (error instanceof Error) { onError({ error }) } + else { onError({ error: `Unexpected Error in sendLLMMessage: ${error}` }); } + ; (_aborter as any)?.() + _didAbort = true + } + + + +} + diff --git a/src/vs/workbench/contrib/void/browser/react/src/util/sendLLMMessage.tsx b/src/vs/workbench/contrib/void/browser/react/src/util/sendLLMMessage.tsx deleted file mode 100644 index 128ffd08..00000000 --- a/src/vs/workbench/contrib/void/browser/react/src/util/sendLLMMessage.tsx +++ /dev/null @@ -1,986 +0,0 @@ -import Anthropic from '@anthropic-ai/sdk'; -import OpenAI from 'openai'; -import { Ollama } from 'ollama/browser' -import { Content, GoogleGenerativeAI, GoogleGenerativeAIFetchError } from '@google/generative-ai'; -import { posthog } from 'posthog-js' -import type { VoidConfig } from '../../../registerConfig.js'; -import type { LLMMessage, OnText, OnError, OnFinalMessage, SendLLMMMessageParams, } from '../../../../../../../platform/void/common/llmMessageTypes.js'; -import { LLMMessageServiceParams } from '../../../../../../../platform/void/common/llmMessageTypes.js'; - -type SendLLMMessageFnTypeInternal = (params: { - messages: LLMMessage[]; - onText: OnText; - onFinalMessage: OnFinalMessage; - onError: OnError; - voidConfig: VoidConfig; - - _setAborter: (aborter: () => void) => void; -}) => void - - -const parseMaxTokensStr = (maxTokensStr: string) => { - // parse the string but only if the full string is a valid number, eg parseInt('100abc') should return NaN - const int = isNaN(Number(maxTokensStr)) ? undefined : parseInt(maxTokensStr) - if (Number.isNaN(int)) - return undefined - return int -} - -// Anthropic -type LLMMessageAnthropic = { - role: 'user' | 'assistant'; - content: string; -} -const sendAnthropicMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter }) => { - - 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 - }); - - - // when receive text - stream.on('text', (newText, fullText) => { - onText({ newText, fullText }) - }) - - // when we get the final message on this stream (or when error/fail) - stream.on('finalMessage', (claude_response) => { - // stringify the response's content - const content = claude_response.content.map(c => c.type === 'text' ? c.text : c.type).join('\n'); - onFinalMessage({ fullText: 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({ error: 'Invalid API key.' }) - } - else { - onError({ error }) - } - }) - - // TODO need to test this to make sure it works, it might throw an error - _setAborter(() => stream.controller.abort()) - -}; - -// Gemini -const sendGeminiMsg: SendLLMMessageFnTypeInternal = async ({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter }) => { - - let fullText = '' - - 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 - const 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 => { - _setAborter(() => response.stream.return(fullText)) - - for await (const chunk of response.stream) { - const newText = chunk.text(); - fullText += newText; - onText({ newText, fullText }); - } - onFinalMessage({ fullText }); - }) - .catch((error) => { - if (error instanceof GoogleGenerativeAIFetchError && error.status === 400) { - onError({ error: 'Invalid API key.' }); - } - else { - onError({ error }); - } - }) -} - -// OpenAI, OpenRouter, OpenAICompatible -const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter }) => { - - let fullText = '' - - let openai: OpenAI - let options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming - - const 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 => { - _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 }); - }) - // when error/fail - this catches errors of both .create() and .then(for await) - .catch(error => { - if (error instanceof OpenAI.APIError && error.status === 401) { - onError({ error: 'Invalid API key.' }); - } - else { - onError({ error }); - } - }) - -}; - -// Ollama -export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter }) => { - - let fullText = '' - - const ollama = new Ollama({ host: voidConfig.ollama.endpoint }) - - ollama.chat({ - model: voidConfig.ollama.model, - messages: messages, - stream: true, - options: { num_predict: parseMaxTokensStr(voidConfig.default.maxTokens) } // this is max_tokens - }) - .then(async stream => { - _setAborter(() => stream.abort()) - // iterate through the stream - for await (const chunk of stream) { - const newText = chunk.message.content; - fullText += newText; - onText({ newText, fullText }); - } - onFinalMessage({ fullText }); - - }) - // when error/fail - .catch(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, _setAborter }) => { - - let fullText = '' - - 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 - // TODO add _setAborter() when add streaming - .then(async responseArr => { - - for (const response of responseArr) { - const type: string = response['type'] - const message = response['message'] - - // when receive text - if (type === 'message') { - fullText += message - onText({ newText: message, fullText }) - } - else if (type === 'sources') { - const { filepath, linestart: _, lineend: _2 } = message as { filepath: string; linestart: number | null; lineend: number | null } - fullText += filepath - onText({ newText: filepath, fullText }) - } - // type: 'status' with an empty 'message' means last message - else if (type === 'status') { - if (!message) { - onFinalMessage({ fullText }) - } - } - } - - }) - .catch(error => { - onError({ error }) - }); - -} - - - - - - -export const sendLLMMessage = ({ - messages, - onText: onText_, - onFinalMessage: onFinalMessage_, - onError: onError_, - abortRef: abortRef_, - voidConfig, - logging: { loggingName } -}: SendLLMMMessageParams) => { - 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() })) - - // only captures number of messages and message "shape", no actual code, instructions, prompts, etc - const captureChatEvent = (eventId: string, extras?: object) => { - posthog.capture(eventId, { - whichApi: voidConfig.default['whichApi'], - numMessages: messages?.length, - messagesShape: messages?.map(msg => ({ role: msg.role, length: msg.content.length })), - version: '2024-11-14', - ...extras, - }) - } - const submit_time = new Date() - - let _fullTextSoFar = '' - let _aborter: (() => void) | null = null - let _setAborter = (fn: () => void) => { _aborter = fn } - let _didAbort = false - - const onText: OnText = ({ newText, fullText }) => { - if (_didAbort) return - onText_({ newText, fullText }) - _fullTextSoFar = fullText - } - - const onFinalMessage: OnFinalMessage = ({ fullText }) => { - if (_didAbort) return - captureChatEvent(`${loggingName} - Received Full Message`, { messageLength: fullText.length, duration: new Date().getMilliseconds() - submit_time.getMilliseconds() }) - onFinalMessage_({ fullText }) - } - - const onError: OnError = ({ error }) => { - console.error('sendLLMMessage onError:', error) - if (_didAbort) return - captureChatEvent(`${loggingName} - Error`, { error }) - onError_({ error }) - } - - const onAbort = () => { - captureChatEvent(`${loggingName} - Abort`, { messageLengthSoFar: _fullTextSoFar.length }) - _aborter?.() - _didAbort = true - } - abortRef_.current = onAbort - - captureChatEvent(`${loggingName} - Sending Message`, { messageLength: messages[messages.length - 1]?.content.length }) - - try { - switch (voidConfig.default.whichApi) { - case 'anthropic': - sendAnthropicMsg({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter, }); - break; - case 'openAI': - case 'openRouter': - case 'openAICompatible': - sendOpenAIMsg({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter, }); - break; - case 'gemini': - sendGeminiMsg({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter, }); - break; - case 'ollama': - sendOllamaMsg({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter, }); - break; - case 'greptile': - sendGreptileMsg({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter, }); - break; - default: - onError({ error: `Error: whichApi was ${voidConfig.default.whichApi}, which is not recognized!` }) - break; - } - } - - catch (error) { - if (error instanceof Error) { onError({ error }) } - else { onError({ error: `Unexpected Error in sendLLMMessage: ${error}` }); } - ; (_aborter as any)?.() - _didAbort = true - } - - - -} - - - - - - - - - - - - - - - - - - -// // 6. Autocomplete -// const autocompleteProvider = new AutocompleteProvider(context); -// context.subscriptions.push(vscode.languages.registerInlineCompletionItemProvider('*', autocompleteProvider)); - -// const voidConfig = getVoidConfigFromPartial(context.globalState.get('partialVoidConfig') ?? {}) - -// // setupAutocomplete({ voidConfig, abortRef }) - -// // 7. Language Server -// console.log('run lsp') -// let disposable = vscode.commands.registerCommand('typeInspector.inspect', runTreeSitter); -// context.subscriptions.push(disposable); - - - - - - - - - - -// import { configFields, VoidConfig } from "../webviews/common/contextForConfig" -// import { FimInfo } from "./sendLLMMessage" - - -// type GetFIMPrompt = ({ voidConfig, fimInfo }: { voidConfig: VoidConfig, fimInfo: FimInfo, }) => string - -// export const getFIMSystem: GetFIMPrompt = ({ voidConfig, fimInfo }) => { - -// switch (voidConfig.default.whichApi) { -// case 'ollama': -// return '' -// case 'anthropic': -// case 'openAI': -// case 'gemini': -// case 'greptile': -// case 'openRouter': -// case 'openAICompatible': -// case 'azure': -// default: -// return `You are given the START and END to a piece of code. Please FILL IN THE MIDDLE between the START and END. - -// Instruction summary: -// 1. Return the MIDDLE of the code between the START and END. -// 2. Do not give an explanation, description, or any other code besides the middle. -// 3. Do not return duplicate code from either START or END. -// 4. Make sure the MIDDLE piece of code has balanced brackets that match the START and END. -// 5. The MIDDLE begins on the same line as START. Please include a newline character if you want to begin on the next line. -// 6. Around 90% of the time, you should return just one or a few lines of code. You should keep your outputs short unless you are confident the user is trying to write boilderplate code. - -// # EXAMPLE - -// ## START: -// \`\`\` python -// def add(a,b): -// return a + b -// def subtract(a,b): -// return a - b -// \`\`\` -// ## END: -// \`\`\` python -// def divide(a,b): -// return a / b -// \`\`\` -// ## EXPECTED OUTPUT: -// \`\`\` python - -// def multiply(a,b): -// return a * b -// \`\`\` - -// # EXAMPLE -// ## START: -// \`\`\` javascript -// const x = 1 - -// const y -// \`\`\` -// ## END: -// \`\`\` javascript - -// const z = 3 -// \`\`\` -// ## EXPECTED OUTPUT: -// \`\`\` javascript -// = 2 -// \`\`\` -// ` -// } - - -// } - - -// export const getFIMPrompt: GetFIMPrompt = ({ voidConfig, fimInfo }) => { - -// const { prefix: fullPrefix, suffix: fullSuffix } = fimInfo -// const prefix = fullPrefix.split('\n').slice(-20).join('\n') -// const suffix = fullSuffix.split('\n').slice(0, 20).join('\n') - - -// console.log('prefix', JSON.stringify(prefix)) -// console.log('suffix', JSON.stringify(suffix)) - -// if (!prefix.trim() && !suffix.trim()) return '' - -// // TODO may want to trim the prefix and suffix -// switch (voidConfig.default.whichApi) { -// case 'ollama': -// if (voidConfig.ollama.model === 'codestral') { -// return `[SUFFIX]${suffix}[PREFIX] ${prefix}` -// } else if (voidConfig.ollama.model.includes('qwen')) { -// return `<|fim_prefix|>${prefix}<|fim_suffix|>${suffix}<|fim_middle|>` -// } -// return '' -// case 'anthropic': -// case 'openAI': -// case 'gemini': -// case 'greptile': -// case 'openRouter': -// case 'openAICompatible': -// case 'azure': -// default: -// return `## START: -// \`\`\` -// ${prefix} -// \`\`\` -// ## END: -// \`\`\` -// ${suffix} -// \`\`\` -// ` -// } -// } - - - - - - - - - - - - - - - - - -// Mathew - sendLLMMessage - -// 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' -// import { getFIMPrompt, getFIMSystem } from './getPrompt'; - -// export type AbortRef = { current: (() => void) } - -// export type LLMMessageOnText = (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 LLMMessageOptions = { stopTokens?: string[] } - -// type SendLLMMessageFnTypeInternal = (params: { -// mode: 'chat' | 'fim', -// messages: LLMMessage[], -// options?: LLMMessageOptions, -// onText: LLMMessageOnText, -// onFinalMessage: OnFinalMessage, -// onError: (error: string) => void, -// abortRef: AbortRef, -// voidConfig: VoidConfig, -// }) => void - - -// type SendLLMMessageFnTypeExternal = (params: ( -// | { mode?: 'chat', messages: LLMMessage[], fimInfo?: undefined, } -// | { mode: 'fim', messages?: undefined, fimInfo: FimInfo, } -// ) & { -// options?: LLMMessageOptions, -// onText: LLMMessageOnText, -// onFinalMessage: OnFinalMessage, -// onError: (error: string) => void, -// abortRef: AbortRef, -// voidConfig: VoidConfig | null, // these may be absent -// }) => void - -// export type FimInfo = { -// prefix: string, -// suffix: string, -// } - -// 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 = ({ options, mode, messages, onText, onFinalMessage, onError, voidConfig, abortRef }) => { - -// let didAbort = false -// let fullText = "" - -// const ollama = new Ollama({ host: voidConfig.ollama.endpoint }) - -// abortRef.current = () => { -// didAbort = true; -// }; - -// type GenerateResponse = Awaited> -// type ChatResponse = Awaited> - - -// // 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(); -// } - -// if (mode === 'fim') { -// // the fim prompt is the last message -// let prompt = messages[messages.length - 1].content -// return ollama.generate({ -// model: voidConfig.ollama.model, -// prompt: prompt, -// stream: true, -// raw: true, -// options: { stop: options?.stopTokens } -// }) -// } - -// 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 -// stream.abort() -// } -// for await (const chunk of stream) { -// if (didAbort) return; - -// const newText = (mode === 'fim' -// ? (chunk as GenerateResponse).response -// : (chunk as ChatResponse).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 = ({ options, mode, messages, fimInfo, onText, onFinalMessage, onError, voidConfig, abortRef }) => { -// if (!voidConfig) -// return onError('No config file found for LLM.'); - -// // handle defaults -// if (!mode) mode = 'chat' -// if (!messages) messages = [] - -// // build messages -// if (mode === 'chat') { -// // nothing needed -// } else if (mode === 'fim') { -// fimInfo = fimInfo! - -// const system = getFIMSystem({ voidConfig, fimInfo }) -// const prompt = getFIMPrompt({ voidConfig, fimInfo }) -// messages = ([ -// { role: 'system', content: system }, -// { role: 'user', content: prompt } -// ] as const) - -// } - -// // trim message content (Anthropic and other providers give an error if there is trailing whitespace) -// messages = messages.map(m => ({ ...m, content: m.content.trim() })) -// .filter(m => m.content !== '') - -// if (messages.length === 0) -// return onError('No messages provided to LLM.'); - -// switch (voidConfig.default.whichApi) { -// case 'anthropic': -// return sendAnthropicMsg({ options, mode, messages, onText, onFinalMessage, onError, voidConfig, abortRef }); -// case 'openAI': -// case 'openRouter': -// case 'openAICompatible': -// return sendOpenAIMsg({ options, mode, messages, onText, onFinalMessage, onError, voidConfig, abortRef }); -// case 'gemini': -// return sendGeminiMsg({ options, mode, messages, onText, onFinalMessage, onError, voidConfig, abortRef }); -// case 'ollama': -// return sendOllamaMsg({ options, mode, messages, onText, onFinalMessage, onError, voidConfig, abortRef }); -// case 'greptile': -// return sendGreptileMsg({ options, mode, messages, onText, onFinalMessage, onError, voidConfig, abortRef }); -// default: -// onError(`Error: whichApi was ${voidConfig.default.whichApi}, which is not recognized!`) -// } - -// } diff --git a/src/vs/workbench/contrib/void/browser/registerConfig.ts b/src/vs/workbench/contrib/void/browser/registerConfig.ts index b1e8708a..4e7f4596 100644 --- a/src/vs/workbench/contrib/void/browser/registerConfig.ts +++ b/src/vs/workbench/contrib/void/browser/registerConfig.ts @@ -26,6 +26,15 @@ const configString = (description: string, defaultVal: string) => { } } +export const parseMaxTokensStr = (maxTokensStr: string) => { + // parse the string but only if the full string is a valid number, eg parseInt('100abc') should return NaN + const int = isNaN(Number(maxTokensStr)) ? undefined : parseInt(maxTokensStr) + if (Number.isNaN(int)) + return undefined + return int +} + + // fields you can customize (don't forget 'default' - it isn't included here!) export const nonDefaultConfigFields = [ 'anthropic', From f3653c1c481f5ed50dc7f05c13cb986f83289a6b Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Wed, 4 Dec 2024 15:07:11 -0800 Subject: [PATCH 3/5] clarify config access --- .../void/electron-main/llmMessageChannel.ts | 2 +- .../workbench/contrib/void/browser/react/build.js | 2 +- .../react/src/sendLLMMessage/anthropic.tsx | 6 ++++-- .../browser/react/src/sendLLMMessage/gemini.tsx | 6 ++++-- .../browser/react/src/sendLLMMessage/greptile.tsx | 8 +++++--- .../browser/react/src/sendLLMMessage/ollama.tsx | 6 ++++-- .../browser/react/src/sendLLMMessage/openai.tsx | 15 +++++++++------ .../contrib/void/browser/react/tsup.config.js | 2 +- 8 files changed, 29 insertions(+), 18 deletions(-) diff --git a/src/vs/platform/void/electron-main/llmMessageChannel.ts b/src/vs/platform/void/electron-main/llmMessageChannel.ts index 9fb7224a..dd437286 100644 --- a/src/vs/platform/void/electron-main/llmMessageChannel.ts +++ b/src/vs/platform/void/electron-main/llmMessageChannel.ts @@ -10,7 +10,7 @@ import { IServerChannel } from '../../../base/parts/ipc/common/ipc.js'; import { Emitter, Event } from '../../../base/common/event.js'; -import { sendLLMMessage } from '../../../workbench/contrib/void/browser/react/out/util/sendLLMMessage.js'; +import { sendLLMMessage } from '../../../workbench/contrib/void/browser/react/out/sendLLMMessage/sendLLMMessage.js'; import { listenerNames, ProxyOnTextPayload, ProxyOnErrorPayload, ProxyOnFinalMessagePayload, ProxyLLMMessageParams, AbortRef, SendLLMMMessageParams, ProxyLLMMessageAbortParams } from '../common/llmMessageTypes.js'; // NODE IMPLEMENTATION OF SENDLLMMESSAGE - calls sendLLMMessage() and returns listeners diff --git a/src/vs/workbench/contrib/void/browser/react/build.js b/src/vs/workbench/contrib/void/browser/react/build.js index 5383c4a4..481591da 100755 --- a/src/vs/workbench/contrib/void/browser/react/build.js +++ b/src/vs/workbench/contrib/void/browser/react/build.js @@ -10,4 +10,4 @@ execSync('npx scope-tailwind ./src -o src2/ -s void-scope -c styles.css -p "pref execSync('npx tsup') -console.log('✅ Done building! Press Cmd+Shift+B again.') +console.log('✅ Done building! Kill your build script(s) (Ctrl+D in them), then press Cmd+Shift+B again.') diff --git a/src/vs/workbench/contrib/void/browser/react/src/sendLLMMessage/anthropic.tsx b/src/vs/workbench/contrib/void/browser/react/src/sendLLMMessage/anthropic.tsx index 4879d2c9..36fa78ac 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sendLLMMessage/anthropic.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sendLLMMessage/anthropic.tsx @@ -9,7 +9,9 @@ type LLMMessageAnthropic = { } export const sendAnthropicMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter }) => { - const anthropic = new Anthropic({ apiKey: voidConfig.anthropic.apikey, dangerouslyAllowBrowser: true }); // defaults to process.env["ANTHROPIC_API_KEY"] + const thisConfig = voidConfig.anthropic + + const anthropic = new Anthropic({ apiKey: thisConfig.apikey, dangerouslyAllowBrowser: true }); // defaults to process.env["ANTHROPIC_API_KEY"] // find system messages and concatenate them const systemMessage = messages @@ -23,7 +25,7 @@ export const sendAnthropicMsg: SendLLMMessageFnTypeInternal = ({ messages, onTex const stream = anthropic.messages.stream({ system: systemMessage, messages: anthropicMessages, - model: voidConfig.anthropic.model, + model: thisConfig.model, max_tokens: parseMaxTokensStr(voidConfig.default.maxTokens)!, // this might be undefined, but it will just throw an error for the user }); diff --git a/src/vs/workbench/contrib/void/browser/react/src/sendLLMMessage/gemini.tsx b/src/vs/workbench/contrib/void/browser/react/src/sendLLMMessage/gemini.tsx index 3f350fee..5a883177 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sendLLMMessage/gemini.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sendLLMMessage/gemini.tsx @@ -6,8 +6,10 @@ export const sendGeminiMsg: SendLLMMessageFnTypeInternal = async ({ messages, on let fullText = '' - const genAI = new GoogleGenerativeAI(voidConfig.gemini.apikey); - const model = genAI.getGenerativeModel({ model: voidConfig.gemini.model }); + const thisConfig = voidConfig.gemini + + const genAI = new GoogleGenerativeAI(thisConfig.apikey); + const model = genAI.getGenerativeModel({ model: thisConfig.model }); // remove system messages that get sent to Gemini // str of all system messages diff --git a/src/vs/workbench/contrib/void/browser/react/src/sendLLMMessage/greptile.tsx b/src/vs/workbench/contrib/void/browser/react/src/sendLLMMessage/greptile.tsx index 81daada3..eb7b4596 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sendLLMMessage/greptile.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sendLLMMessage/greptile.tsx @@ -9,17 +9,19 @@ export const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText let fullText = '' + const thisConfig = voidConfig.greptile + fetch('https://api.greptile.com/v2/query', { method: 'POST', headers: { - 'Authorization': `Bearer ${voidConfig.greptile.apikey}`, - 'X-Github-Token': `${voidConfig.greptile.githubPAT}`, + 'Authorization': `Bearer ${thisConfig.apikey}`, + 'X-Github-Token': `${thisConfig.githubPAT}`, 'Content-Type': `application/json`, }, body: JSON.stringify({ messages, stream: true, - repositories: [voidConfig.greptile.repoinfo], + repositories: [thisConfig.repoinfo], }), }) // this is {message}\n{message}\n{message}...\n diff --git a/src/vs/workbench/contrib/void/browser/react/src/sendLLMMessage/ollama.tsx b/src/vs/workbench/contrib/void/browser/react/src/sendLLMMessage/ollama.tsx index 95f10a2e..44b7ff1e 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sendLLMMessage/ollama.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sendLLMMessage/ollama.tsx @@ -5,12 +5,14 @@ import { SendLLMMessageFnTypeInternal } from './_types.js'; // Ollama export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter }) => { + const thisConfig = voidConfig.ollama + let fullText = '' - const ollama = new Ollama({ host: voidConfig.ollama.endpoint }) + const ollama = new Ollama({ host: thisConfig.endpoint }) ollama.chat({ - model: voidConfig.ollama.model, + model: thisConfig.model, messages: messages, stream: true, options: { num_predict: parseMaxTokensStr(voidConfig.default.maxTokens) } // this is max_tokens diff --git a/src/vs/workbench/contrib/void/browser/react/src/sendLLMMessage/openai.tsx b/src/vs/workbench/contrib/void/browser/react/src/sendLLMMessage/openai.tsx index 545787d1..78d6671a 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sendLLMMessage/openai.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sendLLMMessage/openai.tsx @@ -14,22 +14,25 @@ export const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, const 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 } + const thisConfig = voidConfig.openAI + openai = new OpenAI({ apiKey: thisConfig.apikey, dangerouslyAllowBrowser: true }); + options = { model: thisConfig.model, messages: messages, stream: true, max_completion_tokens: maxTokens } } else if (voidConfig.default.whichApi === 'openRouter') { + const thisConfig = voidConfig.openRouter openai = new OpenAI({ - baseURL: 'https://openrouter.ai/api/v1', apiKey: voidConfig.openRouter.apikey, dangerouslyAllowBrowser: true, + baseURL: 'https://openrouter.ai/api/v1', apiKey: thisConfig.apikey, dangerouslyAllowBrowser: true, defaultHeaders: { 'HTTP-Referer': 'https://voideditor.com', // Optional, for including your app on openrouter.ai rankings. 'X-Title': 'Void Editor', // Optional. Shows in rankings on openrouter.ai. }, }); - options = { model: voidConfig.openRouter.model, messages: messages, stream: true, max_completion_tokens: maxTokens } + options = { model: thisConfig.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 } + const thisConfig = voidConfig.openAICompatible + openai = new OpenAI({ baseURL: thisConfig.endpoint, apiKey: thisConfig.apikey, dangerouslyAllowBrowser: true }) + options = { model: thisConfig.model, messages: messages, stream: true, max_completion_tokens: maxTokens } } else { console.error(`sendOpenAIMsg: invalid whichApi: ${voidConfig.default.whichApi}`) diff --git a/src/vs/workbench/contrib/void/browser/react/tsup.config.js b/src/vs/workbench/contrib/void/browser/react/tsup.config.js index a3432000..45039566 100644 --- a/src/vs/workbench/contrib/void/browser/react/tsup.config.js +++ b/src/vs/workbench/contrib/void/browser/react/tsup.config.js @@ -3,7 +3,7 @@ import { defineConfig } from 'tsup' export default defineConfig({ entry: [ './src2/sidebar-tsx/Sidebar.tsx', - './src2/util/sendLLMMessage.tsx', + './src2/sendLLMMessage/sendLLMMessage.tsx', './src2/util/posthog.tsx', './src2/util/diffLines.tsx', ], From 4b3f881e1f3be837b4777cfd0c365f923f59e663 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Wed, 4 Dec 2024 15:08:40 -0800 Subject: [PATCH 4/5] rm comment --- src/vs/workbench/contrib/void/browser/registerInlineDiffs.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/registerInlineDiffs.ts b/src/vs/workbench/contrib/void/browser/registerInlineDiffs.ts index 60894207..dba595c6 100644 --- a/src/vs/workbench/contrib/void/browser/registerInlineDiffs.ts +++ b/src/vs/workbench/contrib/void/browser/registerInlineDiffs.ts @@ -30,8 +30,6 @@ import { Widget } from '../../../../base/browser/ui/widget.js'; import { URI } from '../../../../base/common/uri.js'; import { LLMMessageServiceParams } from '../../../../platform/void/common/llmMessageTypes.js'; import { ISendLLMMessageService } from '../../../../platform/void/browser/llmMessageService.js'; -// import { ISendLLMMessageService } from '../../../../platform/void/common/sendLLMMessage.js'; -// import { sendLLMMessage } from './react/out/util/sendLLMMessage.js'; // gets converted to --vscode-void-greenBG, see void.css From f2b176dfdab71df9e73ede09cd8a0c90993b14ae Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Wed, 4 Dec 2024 15:12:45 -0800 Subject: [PATCH 5/5] remove `extensions/` code --- .../createJsProgramGraph.ts | 0 .../LangaugeServerTest/findFunctions.ts | 0 extensions/void/src/common/llm/index.ts | 49 -------- .../src/common/llm/providers/anthropic.ts | 66 ---------- .../void/src/common/llm/providers/gemini.ts | 68 ----------- .../void/src/common/llm/providers/greptile.ts | 98 --------------- .../void/src/common/llm/providers/ollama.ts | 113 ------------------ .../void/src/common/llm/providers/openai.ts | 103 ---------------- extensions/void/src/common/llm/types.ts | 26 ---- extensions/void/src/common/llm/utils.ts | 6 - 10 files changed, 529 deletions(-) rename extensions/void/{src/common => }/LangaugeServerTest/createJsProgramGraph.ts (100%) rename extensions/void/{src/common => }/LangaugeServerTest/findFunctions.ts (100%) delete mode 100644 extensions/void/src/common/llm/index.ts delete mode 100644 extensions/void/src/common/llm/providers/anthropic.ts delete mode 100644 extensions/void/src/common/llm/providers/gemini.ts delete mode 100644 extensions/void/src/common/llm/providers/greptile.ts delete mode 100644 extensions/void/src/common/llm/providers/ollama.ts delete mode 100644 extensions/void/src/common/llm/providers/openai.ts delete mode 100644 extensions/void/src/common/llm/types.ts delete mode 100644 extensions/void/src/common/llm/utils.ts diff --git a/extensions/void/src/common/LangaugeServerTest/createJsProgramGraph.ts b/extensions/void/LangaugeServerTest/createJsProgramGraph.ts similarity index 100% rename from extensions/void/src/common/LangaugeServerTest/createJsProgramGraph.ts rename to extensions/void/LangaugeServerTest/createJsProgramGraph.ts diff --git a/extensions/void/src/common/LangaugeServerTest/findFunctions.ts b/extensions/void/LangaugeServerTest/findFunctions.ts similarity index 100% rename from extensions/void/src/common/LangaugeServerTest/findFunctions.ts rename to extensions/void/LangaugeServerTest/findFunctions.ts diff --git a/extensions/void/src/common/llm/index.ts b/extensions/void/src/common/llm/index.ts deleted file mode 100644 index 577c24c3..00000000 --- a/extensions/void/src/common/llm/index.ts +++ /dev/null @@ -1,49 +0,0 @@ -// 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 deleted file mode 100644 index 9a058c4f..00000000 --- a/extensions/void/src/common/llm/providers/anthropic.ts +++ /dev/null @@ -1,66 +0,0 @@ -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 deleted file mode 100644 index fec2ab65..00000000 --- a/extensions/void/src/common/llm/providers/gemini.ts +++ /dev/null @@ -1,68 +0,0 @@ -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 deleted file mode 100644 index 1f66d136..00000000 --- a/extensions/void/src/common/llm/providers/greptile.ts +++ /dev/null @@ -1,98 +0,0 @@ -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 deleted file mode 100644 index b9b548ce..00000000 --- a/extensions/void/src/common/llm/providers/ollama.ts +++ /dev/null @@ -1,113 +0,0 @@ -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 deleted file mode 100644 index 4bbdbb72..00000000 --- a/extensions/void/src/common/llm/providers/openai.ts +++ /dev/null @@ -1,103 +0,0 @@ -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 deleted file mode 100644 index 8b9901b9..00000000 --- a/extensions/void/src/common/llm/types.ts +++ /dev/null @@ -1,26 +0,0 @@ -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 deleted file mode 100644 index 66d0f4df..00000000 --- a/extensions/void/src/common/llm/utils.ts +++ /dev/null @@ -1,6 +0,0 @@ -export const parseMaxTokensStr = (maxTokensStr: string) => { - let int = isNaN(Number(maxTokensStr)) ? undefined : parseInt(maxTokensStr) - if (Number.isNaN(int)) - return undefined - return int -}