diff --git a/extensions/void/package-lock.json b/extensions/void/package-lock.json index 62449b32..2c69af18 100644 --- a/extensions/void/package-lock.json +++ b/extensions/void/package-lock.json @@ -10,6 +10,7 @@ "devDependencies": { "@anthropic-ai/sdk": "^0.29.2", "@eslint/js": "^9.9.1", + "@google/generative-ai": "^0.21.0", "@monaco-editor/react": "^4.6.0", "@rrweb/types": "^2.0.0-alpha.17", "@types/diff": "^5.2.2", @@ -737,6 +738,16 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@google/generative-ai": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@google/generative-ai/-/generative-ai-0.21.0.tgz", + "integrity": "sha512-7XhUbtnlkSEZK15kN3t+tzIMxsbKm/dSkKBFalj+20NvPKe1kBY7mR2P7vuijEn+f06z5+A8bVGKO0v39cr6Wg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", @@ -6013,6 +6024,14 @@ "node": ">=10" } }, + "node_modules/monaco-editor": { + "version": "0.52.0", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.0.tgz", + "integrity": "sha512-OeWhNpABLCeTqubfqLMXGsqf6OmPU6pHM85kF3dhy6kq5hnhuVS1p3VrEW/XhWHc71P2tHyS5JFySD8mgs1crw==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", diff --git a/extensions/void/package.json b/extensions/void/package.json index 16553535..01edcf93 100644 --- a/extensions/void/package.json +++ b/extensions/void/package.json @@ -111,6 +111,7 @@ "devDependencies": { "@anthropic-ai/sdk": "^0.29.2", "@eslint/js": "^9.9.1", + "@google/generative-ai": "^0.21.0", "@monaco-editor/react": "^4.6.0", "@rrweb/types": "^2.0.0-alpha.17", "@types/diff": "^5.2.2", diff --git a/extensions/void/src/common/sendLLMMessage.ts b/extensions/void/src/common/sendLLMMessage.ts index c3feea76..8e4c59dd 100644 --- a/extensions/void/src/common/sendLLMMessage.ts +++ b/extensions/void/src/common/sendLLMMessage.ts @@ -1,6 +1,7 @@ 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 } @@ -37,8 +38,6 @@ type SendLLMMessageFnTypeExternal = (params: { 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) @@ -65,7 +64,7 @@ const sendAnthropicMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFi system: systemMessage, messages: anthropicMessages, model: voidConfig.anthropic.model, - max_tokens: parseInt(voidConfig.default.maxTokens), + max_tokens: parseMaxTokensStr(voidConfig.default.maxTokens)!, // this might be undefined, but it will just throw an error for the user }); let did_abort = false @@ -103,8 +102,62 @@ const sendAnthropicMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFi 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 }) => { @@ -168,7 +221,7 @@ const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinal onError('Invalid API key.'); } else { - onError(error.message); + onError(`${error.name}:\n${error.message}`); } } else { @@ -178,7 +231,6 @@ const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinal }; - // Ollama export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }) => { @@ -196,7 +248,7 @@ export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, model: voidConfig.ollama.model, messages: messages, stream: true, - options: { num_predict: parseInt(voidConfig.default.maxTokens) } // this is max_tokens + options: { num_predict: parseMaxTokensStr(voidConfig.default.maxTokens) } // this is max_tokens }) .then(async stream => { abortRef.current = () => { @@ -220,8 +272,6 @@ export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, }; - - // Greptile // https://docs.greptile.com/api-reference/query // https://docs.greptile.com/quickstart#sample-response-streamed @@ -236,7 +286,6 @@ const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFin didAbort = true } - fetch('https://api.greptile.com/v2/query', { method: 'POST', headers: { @@ -291,8 +340,6 @@ const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFin } - - export const sendLLMMessage: SendLLMMessageFnTypeExternal = ({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }) => { if (!voidConfig) return; @@ -306,6 +353,8 @@ export const sendLLMMessage: SendLLMMessageFnTypeExternal = ({ messages, onText, 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': diff --git a/extensions/void/src/webviews/common/contextForConfig.tsx b/extensions/void/src/webviews/common/contextForConfig.tsx index b535d588..5437bad5 100644 --- a/extensions/void/src/webviews/common/contextForConfig.tsx +++ b/extensions/void/src/webviews/common/contextForConfig.tsx @@ -21,11 +21,12 @@ const configString = (description: string, defaultVal: string) => { export const configFields = [ 'anthropic', 'openAI', + 'gemini', 'greptile', 'ollama', 'openRouter', 'openAICompatible', - 'azure' + 'azure', ] as const @@ -164,6 +165,19 @@ const voidConfigInfo: Record< // } // }, }, + gemini: { + apikey: configString('Google API key.', ''), + model: configEnum( + 'Gemini model to use.', + 'gemini-1.5-flash', + [ + "gemini-1.5-flash", + "gemini-1.5-pro", + "gemini-1.5-flash-8b", + "gemini-1.0-pro" + ] as const + ), + }, } @@ -273,4 +287,3 @@ export function useVoidConfig(): ConfigValueType { } return context } -