diff --git a/.changeset/add-mistral-provider.md b/.changeset/add-mistral-provider.md new file mode 100644 index 000000000..aa192e854 --- /dev/null +++ b/.changeset/add-mistral-provider.md @@ -0,0 +1,12 @@ +--- +"lingo.dev": minor +"@lingo.dev/_compiler": minor +"@lingo.dev/_spec": minor +--- + +feat: add Mistral AI as a supported LLM provider + +- Added Mistral AI provider support across the entire lingo.dev ecosystem +- Users can now use Mistral models for localization by setting MISTRAL_API_KEY +- Supports all Mistral models available through the @ai-sdk/mistral package +- Configuration via environment variable or user-wide config: `npx lingo.dev@latest config set llm.mistralApiKey ` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0e1c19eaf..7adcbbbaf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -18,9 +18,10 @@ Here's how to get the project running locally: - **Node.js**: Make sure you have Node.js version 18 or higher installed. - **pnpm**: You can install using this command `npm install -g pnpm` or by following [this guide](https://pnpm.io/installation) - **AI API Key**: - Currently, Groq and Google are supported. + Currently, Groq, Google, and Mistral are supported. - **GROQ API Key**: You can get one by signing up at [Groq](https://console.groq.com/) - **GOOGLE API Key**: You can get one in the [Google AI Studio](https://aistudio.google.com/apikey) + - **MISTRAL API Key**: You can get one by signing up at [Mistral AI](https://console.mistral.ai) ### Setup @@ -36,7 +37,7 @@ Next, configure an AI API key. You can configure a key in two different ways: **Option A: User-wide (Recommended for development):** -Run one of the following commands that corresponds with the AI provider you want to use in a terminal window. Replace `` with your actual API key. You can configure Groq or Google. +Run one of the following commands that corresponds with the AI provider you want to use in a terminal window. Replace `` with your actual API key. You can configure Groq, Google, or Mistral. Groq: @@ -50,6 +51,12 @@ Google: npx lingo.dev@latest config set llm.googleApiKey ``` +Mistral: + +```bash +npx lingo.dev@latest config set llm.mistralApiKey +``` + This will store the key in your system's user configuration, allowing you to build the project without needing to set it up in each demo directory. **Option B: Project-wide (Alternative):** @@ -73,9 +80,17 @@ echo "GOOGLE_API_KEY=" > demo/next-app/.env echo "GOOGLE_API_KEY=" > demo/vite-project/.env ``` +Mistral: + +```bash +echo "MISTRAL_API_KEY=" > demo/react-router-app/.env +echo "MISTRAL_API_KEY=" > demo/next-app/.env +echo "MISTRAL_API_KEY=" > demo/vite-project/.env +``` + This will create `.env` files in each demo directory with your AI API key set as an environment variable. -_Note:_ When loading LLM API keys (including Groq and Google), the Lingo.dev Compiler checks the following sources in order of priority: +_Note:_ When loading LLM API keys (including Groq, Google, and Mistral), the Lingo.dev Compiler checks the following sources in order of priority: 1. Environment variables (via `process.env`) 2. Environment files (`.env`, `.env.local`, `.env.development`) @@ -132,7 +147,7 @@ Want to add support for a new LLM provider to Lingo.dev? Here's a checklist to h - Update documentation and this contributing guide as needed. **Tip:** -Look at how existing providers like "groq" and "google" are implemented for reference. Consistency helps us maintain quality and predictability! +Look at how existing providers like "groq", "google", and "mistral" are implemented for reference. Consistency helps us maintain quality and predictability! ## Issues diff --git a/packages/cli/package.json b/packages/cli/package.json index 129a951b7..64009e0a6 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -114,6 +114,7 @@ "dependencies": { "@ai-sdk/anthropic": "^1.2.11", "@ai-sdk/google": "^1.2.19", + "@ai-sdk/mistral": "^1.2.8", "@ai-sdk/openai": "^1.3.22", "@babel/generator": "^7.27.1", "@babel/parser": "^7.27.1", diff --git a/packages/cli/src/cli/localizer/explicit.ts b/packages/cli/src/cli/localizer/explicit.ts index d03176dc5..f2ee33dc4 100644 --- a/packages/cli/src/cli/localizer/explicit.ts +++ b/packages/cli/src/cli/localizer/explicit.ts @@ -2,6 +2,7 @@ import { createAnthropic } from "@ai-sdk/anthropic"; import { createGoogleGenerativeAI } from "@ai-sdk/google"; import { createOpenAI } from "@ai-sdk/openai"; import { createOpenRouter } from "@openrouter/ai-sdk-provider"; +import { createMistral } from "@ai-sdk/mistral"; import { I18nConfig } from "@lingo.dev/_spec"; import chalk from "chalk"; import dedent from "dedent"; @@ -73,6 +74,15 @@ export default function createExplicitLocalizer( prompt: provider.prompt, skipAuth: true, }); + case "mistral": + return createAiSdkLocalizer({ + factory: (params) => + createMistral(params).languageModel(provider.model), + id: provider.id, + prompt: provider.prompt, + apiKeyName: "MISTRAL_API_KEY", + baseUrl: provider.baseUrl, + }); } } diff --git a/packages/cli/src/cli/processor/index.ts b/packages/cli/src/cli/processor/index.ts index b1acc6397..702734683 100644 --- a/packages/cli/src/cli/processor/index.ts +++ b/packages/cli/src/cli/processor/index.ts @@ -9,6 +9,7 @@ import { colors } from "../constants"; import { createAnthropic } from "@ai-sdk/anthropic"; import { createGoogleGenerativeAI } from "@ai-sdk/google"; import { createOpenRouter } from "@openrouter/ai-sdk-provider"; +import { createMistral } from "@ai-sdk/mistral"; import { createOllama } from "ollama-ai-provider"; export default function createProcessor( @@ -113,6 +114,17 @@ function getPureModelProvider(provider: I18nConfig["provider"]) { // No API key check needed for Ollama return createOllama()(provider.model); } + case "mistral": { + if (!process.env.MISTRAL_API_KEY) { + throw new Error( + createMissingKeyErrorMessage("Mistral", "MISTRAL_API_KEY"), + ); + } + return createMistral({ + apiKey: process.env.MISTRAL_API_KEY, + baseURL: provider.baseUrl, + })(provider.model); + } default: { throw new Error(createUnsupportedProviderErrorMessage(provider?.id)); } diff --git a/packages/cli/src/cli/utils/settings.ts b/packages/cli/src/cli/utils/settings.ts index 07f18ca5a..8d3db2928 100644 --- a/packages/cli/src/cli/utils/settings.ts +++ b/packages/cli/src/cli/utils/settings.ts @@ -39,6 +39,7 @@ export function getSettings(explicitApiKey: string | undefined): CliSettings { googleApiKey: env.GOOGLE_API_KEY || systemFile.llm?.googleApiKey, openrouterApiKey: env.OPENROUTER_API_KEY || systemFile.llm?.openrouterApiKey, + mistralApiKey: env.MISTRAL_API_KEY || systemFile.llm?.mistralApiKey, }, }; } @@ -73,6 +74,7 @@ const SettingsSchema = Z.object({ groqApiKey: Z.string().optional(), googleApiKey: Z.string().optional(), openrouterApiKey: Z.string().optional(), + mistralApiKey: Z.string().optional(), }), }); @@ -103,6 +105,7 @@ function _loadEnv() { GROQ_API_KEY: Z.string().optional(), GOOGLE_API_KEY: Z.string().optional(), OPENROUTER_API_KEY: Z.string().optional(), + MISTRAL_API_KEY: Z.string().optional(), }) .passthrough() .parse(process.env); @@ -127,6 +130,7 @@ function _loadSystemFile() { groqApiKey: Z.string().optional(), googleApiKey: Z.string().optional(), openrouterApiKey: Z.string().optional(), + mistralApiKey: Z.string().optional(), }).optional(), }) .passthrough() @@ -203,6 +207,12 @@ function _envVarsInfo() { `ℹ️ Using OPENROUTER_API_KEY env var instead of key from user config`, ); } + if (env.MISTRAL_API_KEY && systemFile.llm?.mistralApiKey) { + console.info( + "\x1b[36m%s\x1b[0m", + `ℹ️ Using MISTRAL_API_KEY env var instead of key from user config`, + ); + } if (env.LINGODOTDEV_API_URL) { console.info( "\x1b[36m%s\x1b[0m", diff --git a/packages/compiler/package.json b/packages/compiler/package.json index fdb192db9..35fdaf45d 100644 --- a/packages/compiler/package.json +++ b/packages/compiler/package.json @@ -38,6 +38,7 @@ "dependencies": { "@ai-sdk/google": "^1.2.19", "@ai-sdk/groq": "^1.2.3", + "@ai-sdk/mistral": "^1.2.8", "@babel/generator": "^7.26.5", "@babel/parser": "^7.26.7", "@babel/traverse": "^7.27.4", diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts index ff89110cc..ee94662d1 100644 --- a/packages/compiler/src/index.ts +++ b/packages/compiler/src/index.ts @@ -12,6 +12,8 @@ import { getGroqKeyFromRc, getGoogleKeyFromEnv, getGoogleKeyFromRc, + getMistralKeyFromEnv, + getMistralKeyFromRc, getLingoDotDevKeyFromEnv, getLingoDotDevKeyFromRc, } from "./utils/llm-api-key"; @@ -34,6 +36,10 @@ const keyCheckers: Record< checkEnv: getGoogleKeyFromEnv, checkRc: getGoogleKeyFromRc, }, + mistral: { + checkEnv: getMistralKeyFromEnv, + checkRc: getMistralKeyFromRc, + }, "lingo.dev": { checkEnv: getLingoDotDevKeyFromEnv, checkRc: getLingoDotDevKeyFromRc, diff --git a/packages/compiler/src/lib/lcp/api/index.ts b/packages/compiler/src/lib/lcp/api/index.ts index 272b71914..c789549ed 100644 --- a/packages/compiler/src/lib/lcp/api/index.ts +++ b/packages/compiler/src/lib/lcp/api/index.ts @@ -2,6 +2,7 @@ import { createGroq } from "@ai-sdk/groq"; import { createGoogleGenerativeAI } from "@ai-sdk/google"; import { createOpenRouter } from "@openrouter/ai-sdk-provider"; import { createOllama } from "ollama-ai-provider"; +import { createMistral } from "@ai-sdk/mistral"; import { generateText } from "ai"; import { LingoDotDevEngine } from "@lingo.dev/_sdk"; import { DictionarySchema } from "../schema"; @@ -17,6 +18,8 @@ import { getGoogleKeyFromEnv, getOpenRouterKey, getOpenRouterKeyFromEnv, + getMistralKey, + getMistralKeyFromEnv, getLingoDotDevKeyFromEnv, getLingoDotDevKey, } from "../../../utils/llm-api-key"; @@ -353,9 +356,29 @@ export class LCPAPI { return createOllama()(modelId); } + case "mistral": { + // Specific check for CI/CD or Docker missing Mistral key + if (isRunningInCIOrDocker()) { + const mistralFromEnv = getMistralKeyFromEnv(); + if (!mistralFromEnv) { + this._failMissingLLMKeyCi(providerId); + } + } + const mistralKey = getMistralKey(); + if (!mistralKey) { + throw new Error( + "⚠️ Mistral API key not found. Please set MISTRAL_API_KEY environment variable or configure it user-wide.", + ); + } + console.log( + `Creating Mistral client for ${targetLocale} using model ${modelId}`, + ); + return createMistral({ apiKey: mistralKey })(modelId); + } + default: { throw new Error( - `⚠️ Provider "${providerId}" for locale "${targetLocale}" is not supported. Only "groq" and "google" providers are supported at the moment.`, + `⚠️ Provider "${providerId}" for locale "${targetLocale}" is not supported. Only "groq", "google", "openrouter", "ollama", and "mistral" providers are supported at the moment.`, ); } } diff --git a/packages/compiler/src/lib/lcp/api/provider-details.ts b/packages/compiler/src/lib/lcp/api/provider-details.ts index 0efd845af..cfd8dfbb4 100644 --- a/packages/compiler/src/lib/lcp/api/provider-details.ts +++ b/packages/compiler/src/lib/lcp/api/provider-details.ts @@ -38,4 +38,11 @@ export const providerDetails: Record< getKeyLink: "https://ollama.com/download", docsLink: "https://github.com/ollama/ollama/tree/main/docs", }, + mistral: { + name: "Mistral", + apiKeyEnvVar: "MISTRAL_API_KEY", + apiKeyConfigKey: "llm.mistralApiKey", + getKeyLink: "https://console.mistral.ai", + docsLink: "https://docs.mistral.ai", + }, }; diff --git a/packages/compiler/src/utils/llm-api-key.ts b/packages/compiler/src/utils/llm-api-key.ts index 894c45878..bf1b243ad 100644 --- a/packages/compiler/src/utils/llm-api-key.ts +++ b/packages/compiler/src/utils/llm-api-key.ts @@ -70,3 +70,15 @@ export function getOpenRouterKeyFromRc() { export function getOpenRouterKeyFromEnv() { return getKeyFromEnv("OPENROUTER_API_KEY"); } + +export function getMistralKey() { + return getMistralKeyFromEnv() || getMistralKeyFromRc(); +} + +export function getMistralKeyFromRc() { + return getKeyFromRc("llm.mistralApiKey"); +} + +export function getMistralKeyFromEnv() { + return getKeyFromEnv("MISTRAL_API_KEY"); +} diff --git a/packages/spec/src/config.ts b/packages/spec/src/config.ts index 03a808b3c..e8266cba6 100644 --- a/packages/spec/src/config.ts +++ b/packages/spec/src/config.ts @@ -254,7 +254,14 @@ export const configV1_4Definition = extendConfigDefinition( // v1.4 -> v1.5 // Changes: add "provider" field to the config const providerSchema = Z.object({ - id: Z.enum(["openai", "anthropic", "google", "ollama", "openrouter"]), + id: Z.enum([ + "openai", + "anthropic", + "google", + "ollama", + "openrouter", + "mistral", + ]), model: Z.string(), prompt: Z.string(), baseUrl: Z.string().optional(), diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f33733263..e4c8a5c86 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -235,6 +235,9 @@ importers: '@ai-sdk/google': specifier: ^1.2.19 version: 1.2.19(zod@3.24.1) + '@ai-sdk/mistral': + specifier: ^1.2.8 + version: 1.2.8(zod@3.24.1) '@ai-sdk/openai': specifier: ^1.3.22 version: 1.3.22(zod@3.24.1) @@ -566,6 +569,9 @@ importers: '@ai-sdk/groq': specifier: ^1.2.3 version: 1.2.9(zod@3.24.1) + '@ai-sdk/mistral': + specifier: ^1.2.8 + version: 1.2.8(zod@3.24.1) '@babel/generator': specifier: ^7.26.5 version: 7.27.1 @@ -771,6 +777,12 @@ packages: peerDependencies: zod: ^3.0.0 + '@ai-sdk/mistral@1.2.8': + resolution: {integrity: sha512-lv857D9UJqCVxiq2Fcu7mSPTypEHBUqLl1K+lCaP6X/7QAkcaxI36QDONG+tOhGHJOXTsS114u8lrUTaEiGXbg==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.0.0 + '@ai-sdk/openai@1.3.22': resolution: {integrity: sha512-QwA+2EkG0QyjVR+7h6FE7iOu2ivNqAVMm9UJZkVxxTk5OIq5fFJDTEI/zICEMuHImTTXR2JjsL6EirJ28Jc4cw==} engines: {node: '>=18'} @@ -8984,6 +8996,12 @@ snapshots: '@ai-sdk/provider-utils': 2.2.8(zod@3.24.1) zod: 3.24.1 + '@ai-sdk/mistral@1.2.8(zod@3.24.1)': + dependencies: + '@ai-sdk/provider': 1.1.3 + '@ai-sdk/provider-utils': 2.2.8(zod@3.24.1) + zod: 3.24.1 + '@ai-sdk/openai@1.3.22(zod@3.24.1)': dependencies: '@ai-sdk/provider': 1.1.3 @@ -13497,7 +13515,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.28.0(jiti@2.4.2)): + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0)(eslint@9.28.0(jiti@2.4.2)))(eslint@9.28.0(jiti@2.4.2)): dependencies: debug: 3.2.7 optionalDependencies: @@ -13519,7 +13537,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.28.0(jiti@2.4.2) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.28.0(jiti@2.4.2)) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0)(eslint@9.28.0(jiti@2.4.2)))(eslint@9.28.0(jiti@2.4.2)) hasown: 2.0.2 is-core-module: 2.15.1 is-glob: 4.0.3