diff --git a/extensions/void/src/common/sendLLMMessage.ts b/extensions/void/src/common/sendLLMMessage.ts deleted file mode 100644 index 51c8633c..00000000 --- a/extensions/void/src/common/sendLLMMessage.ts +++ /dev/null @@ -1,432 +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' -import Groq from 'groq-sdk'; - -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) - }); - -} - -const sendGroqMsg: SendLLMMessageFnTypeInternal = async ({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }) => { - let didAbort = false; - let fullText = ''; - - abortRef.current = () => { - didAbort = true; - }; - - const groq = new Groq({ - apiKey: voidConfig.groq.apikey, - dangerouslyAllowBrowser: true - }); - - - try { - const stream = await groq.chat.completions.create({ - messages: messages, - model: voidConfig.groq.model, - stream: true, - temperature: 0.7, - max_tokens: parseMaxTokensStr(voidConfig.default.maxTokens), - }); - - for await (const chunk of stream) { - if (didAbort) { - break; - } - - const newText = chunk.choices[0]?.delta?.content || ''; - if (newText) { - fullText += newText; - onText(newText, fullText); - } - } - - if (!didAbort) { - onFinalMessage(fullText); - } - } catch (error: any) { - if (error?.status === 401) { - onError('Invalid API key.'); - } else { - onError(error.message || 'An error occurred while connecting to Groq.'); - } - } -}; - -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 }); - case 'groq': - return sendGroqMsg({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }); - default: - onError(`Error: whichApi was ${voidConfig.default.whichApi}, which is not recognized!`) - } -} diff --git a/extensions/void/src/webviews/common/contextForConfig.tsx b/extensions/void/src/webviews/common/contextForConfig.tsx deleted file mode 100644 index 5288e51d..00000000 --- a/extensions/void/src/webviews/common/contextForConfig.tsx +++ /dev/null @@ -1,302 +0,0 @@ -import React, { ReactNode, createContext, useCallback, useContext, useEffect, useRef, useState, } from "react" -import { awaitVSCodeResponse, getVSCodeAPI, useOnVSCodeMessage } from "./getVscodeApi" - -const configEnum = (description: string, defaultVal: EnumArr[number], enumArr: EnumArr) => { - return { - description, - defaultVal, - enumArr, - } -} - -const configString = (description: string, defaultVal: string) => { - return { - description, - defaultVal, - enumArr: undefined, - } -} - -// fields you can customize (don't forget 'default' - it isn't included here!) -export const configFields = [ - 'anthropic', - 'openAI', - 'groq', - 'gemini', - 'greptile', - 'ollama', - 'openRouter', - 'openAICompatible', - 'azure', -] as const - - - -const voidConfigInfo: Record< - typeof configFields[number] | 'default', { - [prop: string]: { - description: string, - enumArr?: readonly string[] | undefined, - defaultVal: string, - }, - } -> = { - default: { - whichApi: configEnum( - "API Provider.", - 'anthropic', - configFields, - ), - - maxTokens: configEnum( - "Max number of tokens to output.", - '1024', - [ - "default", // this will be parseInt'd into NaN and ignored by the API. Anything that's not a number has this behavior. - "1024", - "2048", - "4096", - "8192" - ] as const, - ), - - }, - anthropic: { - apikey: configString('Anthropic API key.', ''), - model: configEnum( - "Anthropic model to use.", - 'claude-3-5-sonnet-20240620', - [ - "claude-3-5-sonnet-20240620", - "claude-3-opus-20240229", - "claude-3-sonnet-20240229", - "claude-3-haiku-20240307" - ] as const, - ), - }, - openAI: { - apikey: configString('OpenAI API key.', ''), - model: configEnum( - 'OpenAI model to use.', - 'gpt-4o', - [ - "o1-preview", - "o1-mini", - "gpt-4o", - "gpt-4o-2024-05-13", - "gpt-4o-2024-08-06", - "gpt-4o-mini", - "gpt-4o-mini-2024-07-18", - "gpt-4-turbo", - "gpt-4-turbo-2024-04-09", - "gpt-4-turbo-preview", - "gpt-4-0125-preview", - "gpt-4-1106-preview", - "gpt-4", - "gpt-4-0613", - "gpt-3.5-turbo-0125", - "gpt-3.5-turbo", - "gpt-3.5-turbo-1106" - ] as const - ), - }, - greptile: { - apikey: configString('Greptile API key.', ''), - githubPAT: configString('Github PAT that Greptile uses to access your repository', ''), - remote: configEnum( - 'Repo location', - 'github', - [ - 'github', - 'gitlab' - ] as const - ), - repository: configString('Repository identifier in "owner/repository" format.', ''), - branch: configString('Name of the branch to use.', 'main'), - }, - ollama: { - endpoint: configString( - 'The endpoint of your Ollama instance. Start Ollama by running `OLLAMA_ORIGINS="vscode-webview://*" ollama serve`.', - 'http://127.0.0.1:11434' - ), - // TODO we should allow user to select model inside Void, but for now we'll just let them handle the Ollama setup on their own - model: configEnum( - 'Ollama model to use.', - 'codestral', - ["codestral", "codegemma", "codegemma:2b", "codegemma:7b", "codellama", "codellama:7b", "codellama:13b", "codellama:34b", "codellama:70b", "codellama:code", "codellama:python", "command-r", "command-r:35b", "command-r-plus", "command-r-plus:104b", "deepseek-coder-v2", "deepseek-coder-v2:16b", "deepseek-coder-v2:236b", "falcon2", "falcon2:11b", "firefunction-v2", "firefunction-v2:70b", "gemma", "gemma:2b", "gemma:7b", "gemma2", "gemma2:2b", "gemma2:9b", "gemma2:27b", "llama2", "llama2:7b", "llama2:13b", "llama2:70b", "llama3", "llama3:8b", "llama3:70b", "llama3-chatqa", "llama3-chatqa:8b", "llama3-chatqa:70b", "llama3-gradient", "llama3-gradient:8b", "llama3-gradient:70b", "llama3.1", "llama3.2", "llama3.1:8b", "llama3.1:70b", "llama3.1:405b", "llava", "llava:7b", "llava:13b", "llava:34b", "llava-llama3", "llava-llama3:8b", "llava-phi3", "llava-phi3:3.8b", "mistral", "mistral:7b", "mistral-large", "mistral-large:123b", "mistral-nemo", "mistral-nemo:12b", "mixtral", "mixtral:8x7b", "mixtral:8x22b", "moondream", "moondream:1.8b", "openhermes", "openhermes:v2.5", "phi3", "phi3:3.8b", "phi3:14b", "phi3.5", "phi3.5:3.8b", "qwen", "qwen:7b", "qwen:14b", "qwen:32b", "qwen:72b", "qwen:110b", "qwen2", "qwen2:0.5b", "qwen2:1.5b", "qwen2:7b", "qwen2:72b", "smollm", "smollm:135m", "smollm:360m", "smollm:1.7b"] as const - ), - }, - openRouter: { - model: configString( - 'OpenRouter model to use.', - 'openai/gpt-4o' - ), - apikey: configString('OpenRouter API key.', ''), - }, - openAICompatible: { - endpoint: configString('The baseUrl (exluding /chat/completions).', 'http://127.0.0.1:11434/v1'), - model: configString('The name of the model to use.', 'gpt-4o'), - apikey: configString('Your API key.', ''), - }, - groq: { - apikey: configString('Groq API key.', ''), - model: configEnum( - 'Groq model to use.', - 'mixtral-8x7b-32768', - [ - "mixtral-8x7b-32768", - "llama2-70b-4096", - "gemma-7b-it" - ] as const - ), - }, - azure: { - // "void.azure.apiKey": { - // "type": "string", - // "description": "Azure API key." - // }, - // "void.azure.deploymentId": { - // "type": "string", - // "description": "Azure API deployment ID." - // }, - // "void.azure.resourceName": { - // "type": "string", - // "description": "Name of the Azure OpenAI resource. Either this or `baseURL` can be used. \nThe resource name is used in the assembled URL: `https://{resourceName}.openai.azure.com/openai/deployments/{modelId}{path}`" - // }, - // "void.azure.providerSettings": { - // "type": "object", - // "properties": { - // "baseURL": { - // "type": "string", - // "default": "https://${resourceName}.openai.azure.com/openai/deployments", - // "description": "Azure API base URL." - // }, - // "headers": { - // "type": "object", - // "description": "Custom headers to include in the requests." - // } - // } - // }, - }, - 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 - ), - }, -} - - -// this is the type that comes with metadata like desc, default val, etc -type VoidConfigInfo = typeof voidConfigInfo -export type VoidConfigField = keyof typeof voidConfigInfo // typeof configFields[number] - -// this is the type that specifies the user's actual config -export type PartialVoidConfig = { - [K in keyof typeof voidConfigInfo]?: { - [P in keyof typeof voidConfigInfo[K]]?: typeof voidConfigInfo[K][P]['defaultVal'] - } -} - -export type VoidConfig = { - [K in keyof typeof voidConfigInfo]: { - [P in keyof typeof voidConfigInfo[K]]: typeof voidConfigInfo[K][P]['defaultVal'] - } -} - - - -export const getVoidConfigFromPartial = (partialVoidConfig: PartialVoidConfig): VoidConfig => { - const config = {} as PartialVoidConfig - for (let field of [...configFields, 'default'] as const) { - config[field] = {} - for (let prop in voidConfigInfo[field]) { - config[field][prop] = partialVoidConfig[field]?.[prop]?.trim() || voidConfigInfo[field][prop].defaultVal - } - } - return config as VoidConfig -} - -const defaultVoidConfig: VoidConfig = getVoidConfigFromPartial({}) - -// const [stateRef, setState] = useInstantState(initVal) -// setState instantly changes the value of stateRef instead of having to wait until the next render -const useInstantState = (initVal: T) => { - const stateRef = useRef(initVal) - const [_, setS] = useState(initVal) - const setState = useCallback((newVal: T) => { - setS(newVal); - stateRef.current = newVal; - }, []) - return [stateRef as React.RefObject, setState] as const // make s.current readonly - setState handles all changes -} - - - -type SetConfigParamType = (field: K, param: keyof VoidConfigInfo[K], newVal: string) => void - -type ConfigValueType = { - voidConfig: VoidConfig, - voidConfigInfo: VoidConfigInfo, - partialVoidConfig: PartialVoidConfig, - setConfigParam: SetConfigParamType -} - - -const ConfigContext = createContext(undefined as unknown as ConfigValueType) - -export function ConfigProvider({ children }: { children: ReactNode }) { - const [partialVoidConfig, setPartialVoidConfig] = useInstantState({}) // the user's selections - const [voidConfig, setVoidConfig] = useState(defaultVoidConfig) - - - // get the config on mount - useEffect(() => { - getVSCodeAPI().postMessage({ type: 'getPartialVoidConfig' }) - awaitVSCodeResponse('partialVoidConfig').then((m) => { - setPartialVoidConfig(m.partialVoidConfig) - const newFullConfig = getVoidConfigFromPartial(m.partialVoidConfig) - setVoidConfig(newFullConfig) - }) - }, [setPartialVoidConfig]) - - // return the provider - return ( { - const newPartialConfig: PartialVoidConfig = { - ...partialVoidConfig.current, - [field]: { - ...partialVoidConfig.current?.[field], - [param]: newVal - } - } - setPartialVoidConfig(newPartialConfig) - const newFullConfig = getVoidConfigFromPartial(newPartialConfig) - setVoidConfig(newFullConfig) - getVSCodeAPI().postMessage({ type: 'persistPartialVoidConfig', partialVoidConfig: newPartialConfig }) - } - }} - > - {children} - - ) -} - -export function useVoidConfig(): ConfigValueType { - const context = useContext(ConfigContext) - if (context === undefined) { - throw new Error("useVoidConfig missing Provider") - } - return context -} diff --git a/package-lock.json b/package-lock.json index 87c05495..afc964f6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,6 +35,7 @@ "@xterm/addon-webgl": "^0.19.0-beta.64", "@xterm/headless": "^5.6.0-beta.64", "@xterm/xterm": "^5.6.0-beta.64", + "groq-sdk": "^0.9.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", "jschardet": "3.1.3", @@ -3103,7 +3104,6 @@ "version": "20.14.13", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.13.tgz", "integrity": "sha512-+bHoGiZb8UiQ0+WEtmph2IWQCjIqg8MDZMAV+ppRRhUZnquF5mQkP/9vpSwJClEiSM/C7fZZExPzfU0vJTyp8w==", - "dev": true, "dependencies": { "undici-types": "~5.26.4" } @@ -3112,7 +3112,6 @@ "version": "2.6.11", "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.11.tgz", "integrity": "sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*", @@ -4507,7 +4506,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", - "dev": true, "license": "MIT", "dependencies": { "event-target-shim": "^5.0.0" @@ -4588,7 +4586,6 @@ "version": "4.5.0", "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz", "integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==", - "dev": true, "license": "MIT", "dependencies": { "humanize-ms": "^1.2.1" @@ -5143,8 +5140,7 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k= sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k= sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "node_modules/atob": { "version": "2.1.2", @@ -6288,7 +6284,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -6300,7 +6295,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk= sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, "engines": { "node": ">=0.4.0" } @@ -8070,7 +8064,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -8949,7 +8942,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dev": true, "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -8963,7 +8955,6 @@ "version": "1.7.2", "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==", - "dev": true, "license": "MIT" }, "node_modules/format": { @@ -8979,7 +8970,6 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", - "dev": true, "license": "MIT", "dependencies": { "node-domexception": "1.0.0", @@ -9893,6 +9883,30 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, + "node_modules/groq-sdk": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/groq-sdk/-/groq-sdk-0.9.0.tgz", + "integrity": "sha512-Kdbl65yOwd5ga+UZd3KCULnoOlcPV7qJ+FfemivVpu3bOZeT7WJWh/ywAuivwJ7aJzcXmwf6x5t9x0MV0ZmR0A==", + "license": "Apache-2.0", + "dependencies": { + "@types/node": "^18.11.18", + "@types/node-fetch": "^2.6.4", + "abort-controller": "^3.0.0", + "agentkeepalive": "^4.2.1", + "form-data-encoder": "1.7.2", + "formdata-node": "^4.3.2", + "node-fetch": "^2.6.7" + } + }, + "node_modules/groq-sdk/node_modules/@types/node": { + "version": "18.19.67", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.67.tgz", + "integrity": "sha512-wI8uHusga+0ZugNp0Ol/3BqQfEcCCNfojtO6Oou9iVNGPTL6QNSdnUdqq85fRgIorLhLMuPIKpsN98QE9Nh+KQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, "node_modules/gulp": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/gulp/-/gulp-4.0.2.tgz", @@ -12140,7 +12154,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.0.0" @@ -14715,7 +14728,6 @@ "version": "1.45.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.45.0.tgz", "integrity": "sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w==", - "dev": true, "engines": { "node": ">= 0.6" } @@ -14724,7 +14736,6 @@ "version": "2.1.28", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.28.tgz", "integrity": "sha512-0TO2yJ5YHYr7M2zzT7gDU1tbwHxEUWBCLt0lscSNpcdAfFyJOVEpRYNS7EXVcTLNj/25QO8gulHC5JtTzSE2UQ==", - "dev": true, "dependencies": { "mime-db": "1.45.0" }, @@ -15339,7 +15350,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", - "dev": true, "funding": [ { "type": "github", @@ -15359,7 +15369,6 @@ "version": "2.6.8", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.8.tgz", "integrity": "sha512-RZ6dBYuj8dRSfxpUSu+NsdF1dpPpluJxwOp+6IoDp/sH2QNDSvurYsAa+F1WxY2RjA1iP93xhcsUoYbF2XBqVg==", - "dev": true, "dependencies": { "whatwg-url": "^5.0.0" }, @@ -21199,8 +21208,7 @@ "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "dev": true + "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, "node_modules/tree-kill": { "version": "1.2.2", @@ -21794,8 +21802,7 @@ "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" }, "node_modules/union-value": { "version": "1.0.1", @@ -22269,7 +22276,6 @@ "version": "4.0.0-beta.3", "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", - "dev": true, "license": "MIT", "engines": { "node": ">= 14" @@ -22291,8 +22297,7 @@ "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE= sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "dev": true + "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE= sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, "node_modules/webpack": { "version": "5.94.0", @@ -22560,7 +22565,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0= sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "dev": true, "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" diff --git a/package.json b/package.json index dc1b4e96..968a9b1d 100644 --- a/package.json +++ b/package.json @@ -97,6 +97,7 @@ "@xterm/addon-webgl": "^0.19.0-beta.64", "@xterm/headless": "^5.6.0-beta.64", "@xterm/xterm": "^5.6.0-beta.64", + "groq-sdk": "^0.9.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", "jschardet": "3.1.3", 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 index 128ffd08..328df856 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/util/sendLLMMessage.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/util/sendLLMMessage.tsx @@ -3,9 +3,10 @@ import OpenAI from 'openai'; import { Ollama } from 'ollama/browser' import { Content, GoogleGenerativeAI, GoogleGenerativeAIFetchError } from '@google/generative-ai'; import { posthog } from 'posthog-js' +import Groq, { GroqError } from 'groq-sdk' + 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[]; @@ -211,6 +212,44 @@ export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, }; +// Groq +const sendGroqMsg: SendLLMMessageFnTypeInternal = async ({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter }) => { + let fullText = ''; + + const groq = new Groq({ + apiKey: voidConfig.groq.apikey, + dangerouslyAllowBrowser: true + }); + + await groq.chat.completions + .create({ + messages: messages, + model: voidConfig.groq.model, + stream: true, + temperature: 0.7, + max_tokens: parseMaxTokensStr(voidConfig.default.maxTokens), + }) + .then(async response => { + _setAborter(() => response.controller.abort()) + // when receive text + for await (const chunk of response) { + const newText = chunk.choices[0]?.delta?.content || ''; + if (newText) { + fullText += newText; + onText({ newText, fullText }); + } + } + + onFinalMessage({ fullText }); + }) + .catch(error => { + onError({ error }); + }) + + +}; + + // Greptile // https://docs.greptile.com/api-reference/query // https://docs.greptile.com/quickstart#sample-response-streamed diff --git a/src/vs/workbench/contrib/void/browser/registerConfig.ts b/src/vs/workbench/contrib/void/browser/registerConfig.ts index b1e8708a..3cd04aa3 100644 --- a/src/vs/workbench/contrib/void/browser/registerConfig.ts +++ b/src/vs/workbench/contrib/void/browser/registerConfig.ts @@ -32,6 +32,7 @@ export const nonDefaultConfigFields = [ 'openAI', 'gemini', 'greptile', + 'groq', 'ollama', 'openRouter', 'openAICompatible', @@ -122,6 +123,18 @@ const voidConfigInfo: Record< repository: configString('Repository identifier in "owner / repository" format.', ''), branch: configString('Name of the branch to use.', 'main'), }, + groq: { + apikey: configString('Groq API key.', ''), + model: configEnum( + 'Groq model to use.', + 'mixtral-8x7b-32768', + [ + "mixtral-8x7b-32768", + "llama2-70b-4096", + "gemma-7b-it" + ] as const + ), + }, ollama: { endpoint: configString( 'The endpoint of your Ollama instance. Start Ollama by running `OLLAMA_ORIGINS="vscode - webview://*" ollama serve`.',