diff --git a/.gitignore b/.gitignore index 5daf304c..fcdb9b6f 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ product.overrides.json *.snap.actual .vscode-test .tmp/ +.tool-versions diff --git a/package-lock.json b/package-lock.json index 4a54fcf0..e7570e5e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "void-dev", - "version": "1.0.0", + "version": "1.94.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "void-dev", - "version": "1.0.0", + "version": "1.94.0", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -14,6 +14,7 @@ "@google/generative-ai": "^0.21.0", "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", + "@mistralai/mistralai": "^1.3.5", "@parcel/watcher": "2.1.0", "@rrweb/record": "^2.0.0-alpha.17", "@rrweb/types": "^2.0.0-alpha.17", @@ -65,7 +66,8 @@ "vscode-regexpp": "^3.1.0", "vscode-textmate": "9.1.0", "yauzl": "^3.0.0", - "yazl": "^2.4.3" + "yazl": "^2.4.3", + "zod": "^3.24.1" }, "devDependencies": { "@playwright/test": "^1.46.1", @@ -1974,6 +1976,16 @@ "exenv-es6": "^1.1.1" } }, + "node_modules/@mistralai/mistralai": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-1.3.5.tgz", + "integrity": "sha512-yC91oJ5ScEPqbXmv3mJTwTFgu/ZtsYoOPOhaVXSsy6x4zXTqTI57yEC1flC9uiA8GpG/yhpn2BBUXF95+U9Blw==", + "peerDependencies": { + "react": "^18 || ^19", + "react-dom": "^18 || ^19", + "zod": ">= 3" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -23329,6 +23341,15 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.24.1", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz", + "integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json index d0677769..3f53de4a 100644 --- a/package.json +++ b/package.json @@ -79,6 +79,7 @@ "@google/generative-ai": "^0.21.0", "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", + "@mistralai/mistralai": "^1.3.5", "@parcel/watcher": "2.1.0", "@rrweb/record": "^2.0.0-alpha.17", "@rrweb/types": "^2.0.0-alpha.17", @@ -130,7 +131,8 @@ "vscode-regexpp": "^3.1.0", "vscode-textmate": "9.1.0", "yauzl": "^3.0.0", - "yazl": "^2.4.3" + "yazl": "^2.4.3", + "zod": "^3.24.1" }, "devDependencies": { "@playwright/test": "^1.46.1", diff --git a/src/package-lock.json b/src/package-lock.json new file mode 100644 index 00000000..3b02711d --- /dev/null +++ b/src/package-lock.json @@ -0,0 +1,62 @@ +{ + "name": "src", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "@mistralai/mistralai": "^1.3.5", + "zod": "^3.23.8" + } + }, + "node_modules/@mistralai/mistralai": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-1.3.5.tgz", + "integrity": "sha512-yC91oJ5ScEPqbXmv3mJTwTFgu/ZtsYoOPOhaVXSsy6x4zXTqTI57yEC1flC9uiA8GpG/yhpn2BBUXF95+U9Blw==", + "peerDependencies": { + "react": "^18 || ^19", + "react-dom": "^18 || ^19", + "zod": ">= 3" + } + }, + "node_modules/react": { + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz", + "integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz", + "integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "scheduler": "^0.25.0" + }, + "peerDependencies": { + "react": "^19.0.0" + } + }, + "node_modules/scheduler": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz", + "integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==", + "license": "MIT", + "peer": true + }, + "node_modules/zod": { + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/src/vs/platform/void/common/voidSettingsTypes.ts b/src/vs/platform/void/common/voidSettingsTypes.ts index 6814c8e7..35a20c35 100644 --- a/src/vs/platform/void/common/voidSettingsTypes.ts +++ b/src/vs/platform/void/common/voidSettingsTypes.ts @@ -87,7 +87,18 @@ export const defaultGeminiModels = modelInfoOfDefaultNames([ 'gemini-1.0-pro' ]) - +export const defaultMistralModels = modelInfoOfDefaultNames([ + "open-codestral-mamba", + "open-mistral-nemo", + "pixtral-12b-2409", + "mistral-large-latest", + "pixtral-large-latest", + "ministral-3b-latest", + "ministral-8b-latest", + "mistral-small-latest", + "codestral-latest", + "mistral-embed" +]) // export const parseMaxTokensStr = (maxTokensStr: string) => { // // parse the string but only if the full string is a valid number, eg parseInt('100abc') should return NaN @@ -137,6 +148,9 @@ export const defaultProviderSettings = { apiKey: '', }, groq: { + apiKey: '', + }, + mistral: { apiKey: '' } } as const @@ -216,6 +230,11 @@ export const displayInfoOfProviderName = (providerName: ProviderName): DisplayIn title: 'Groq', } } + else if (providerName === 'mistral') { + return { + title: 'Mistral', + } + } throw new Error(`descOfProviderName: Unknown provider name: "${providerName}"`) } @@ -234,16 +253,18 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName providerName === 'openRouter' ? 'sk-or-key...' : // sk-or-v1-key providerName === 'gemini' ? 'key...' : providerName === 'groq' ? 'gsk_key...' : - providerName === 'openAICompatible' ? 'sk-key...' : - '(never)', + providerName === 'mistral' ? 'api-key...' : + providerName === 'openAICompatible' ? 'sk-key...' : + '(never)', subTextMd: providerName === 'anthropic' ? 'Get your [API Key here](https://console.anthropic.com/settings/keys).' : providerName === 'openAI' ? 'Get your [API Key here](https://platform.openai.com/api-keys).' : providerName === 'openRouter' ? 'Get your [API Key here](https://openrouter.ai/settings/keys).' : providerName === 'gemini' ? 'Get your [API Key here](https://aistudio.google.com/apikey).' : providerName === 'groq' ? 'Get your [API Key here](https://console.groq.com/keys).' : - providerName === 'openAICompatible' ? 'Add any OpenAI-Compatible endpoint.' : - undefined, + providerName === 'mistral' ? 'Get your [API Key here](https://console.mistral.ai/api-keys/).' : + providerName === 'openAICompatible' ? undefined : + undefined, } } else if (settingName === 'endpoint') { @@ -285,6 +306,8 @@ const defaultCustomSettings: Record = { endpoint: undefined, } + + export const voidInitModelOptions = { anthropic: { models: defaultAnthropicModels, @@ -307,6 +330,9 @@ export const voidInitModelOptions = { groq: { models: defaultGroqModels, }, + mistral: { + models: defaultMistralModels, + } } @@ -354,6 +380,12 @@ export const defaultSettingsOfProvider: SettingsOfProvider = { ...voidInitModelOptions.openAICompatible, _enabled: undefined, }, + mistral: { + ...defaultCustomSettings, + ...defaultProviderSettings.mistral, + ...voidInitModelOptions.mistral, + _enabled: undefined, + } } diff --git a/src/vs/platform/void/electron-main/llmMessage/mistral.ts b/src/vs/platform/void/electron-main/llmMessage/mistral.ts new file mode 100644 index 00000000..9a32e2b3 --- /dev/null +++ b/src/vs/platform/void/electron-main/llmMessage/mistral.ts @@ -0,0 +1,109 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Glass Devtools, Inc. All rights reserved. + * Mistral implementation by Jérôme Commaret (https://github.com/jcommaret) + * Void Editor additions licensed under the AGPL 3.0 License. + *--------------------------------------------------------------------------------------------*/ + +import { Mistral } from '@mistralai/mistralai'; +import { _InternalSendLLMMessageFnType } from '../../common/llmMessageTypes.js'; + +interface MistralMessage { + role: 'user' | 'assistant'; + content: string; +} + +interface MistralChunk { + data: { + id: string; + object: string; + created: number; + model: string; + choices: Array<{ + index: number; + delta: { + content?: string; + role?: string; + }; + finishReason: string | null; + }>; + }; +} + +// Mistral +export const sendMistralMsg: _InternalSendLLMMessageFnType = async ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => { + let fullText = ''; + + const thisConfig = settingsOfProvider.mistral; + + if (!thisConfig.apiKey) { + onError({ message: 'Mistral API key not configured.', fullError: new Error('No API key') }); + return; + } + + const mistral = new Mistral({ + apiKey: thisConfig.apiKey + }); + + try { + // Check if there are messages to process + if (!messages || messages.length === 0) { + onError({ message: 'No messages to process.', fullError: new Error('No messages provided') }); + return; + } + + // Convert messages for Mistral + const mistralMessages = messages + .filter(msg => msg.role !== 'system') // Ignore system messages + .map(msg => ({ + role: msg.role === 'assistant' ? 'assistant' : 'user', + content: msg.content.trim() + })) as MistralMessage[]; + + // Ensure there is at least one message + if (mistralMessages.length === 0) { + onError({ message: 'No valid messages to send.', fullError: new Error('No valid messages') }); + return; + } + + // Ensure the last message is from the user + if (mistralMessages[mistralMessages.length - 1].role === 'assistant') { + mistralMessages.push({ + role: 'user', + content: 'Continue.' + }); + } + + const stream = await mistral.chat.stream({ + model: modelName, + messages: mistralMessages, + temperature: 0.7, + maxTokens: 2048 + }); + + _setAborter(() => { }); // Mistral does not provide an abort method + + for await (const chunk of stream) { + if (typeof chunk === 'object' && chunk && 'data' in chunk) { + const { data } = chunk as MistralChunk; + if (data.choices?.[0]?.delta?.content) { + const newText = data.choices[0].delta.content; + fullText += newText; + onText({ newText, fullText }); + } + } + } + + if (!fullText) { + onError({ message: 'No response received from Mistral.', fullError: new Error('No response content') }); + return; + } + + onFinalMessage({ fullText }); + } catch (error: any) { + const errorMessage = error.message || JSON.stringify(error); + onError({ + message: `Mistral Error: ${errorMessage}`, + fullError: error + }); + } +}; diff --git a/src/vs/platform/void/electron-main/llmMessage/sendLLMMessage.ts b/src/vs/platform/void/electron-main/llmMessage/sendLLMMessage.ts index 918271e6..0d236e50 100644 --- a/src/vs/platform/void/electron-main/llmMessage/sendLLMMessage.ts +++ b/src/vs/platform/void/electron-main/llmMessage/sendLLMMessage.ts @@ -11,6 +11,7 @@ import { sendOllamaMsg } from './ollama.js'; import { sendOpenAIMsg } from './openai.js'; import { sendGeminiMsg } from './gemini.js'; import { sendGroqMsg } from './groq.js'; +import { sendMistralMsg } from './mistral.js'; export const sendLLMMessage = ({ messages, @@ -96,6 +97,9 @@ export const sendLLMMessage = ({ case 'groq': sendGroqMsg({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName }); break; + case 'mistral': + sendMistralMsg({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName }); + break; default: onError({ message: `Error: Void provider was "${providerName}", which is not recognized.`, fullError: null }) break;