feat: Provider/Mistral full support for Mistral on OpenClaw 🇫🇷 (#23845)

* Onboard: add Mistral auth choice and CLI flags

* Onboard/Auth: add Mistral provider config defaults

* Auth choice: wire Mistral API-key flow

* Onboard non-interactive: support --mistral-api-key

* Media understanding: add Mistral Voxtral audio provider

* Changelog: note Mistral onboarding and media support

* Docs: add Mistral provider and onboarding/media references

* Tests: cover Mistral media registry/defaults and auth mapping

* Memory: add Mistral embeddings provider support

* Onboarding: refresh Mistral model metadata

* Docs: document Mistral embeddings and endpoints

* Memory: persist Mistral embedding client state in managers

* Memory: add regressions for mistral provider wiring

* Gateway: add live tool probe retry helper

* Gateway: cover live tool probe retry helper

* Gateway: retry malformed live tool-read probe responses

* Memory: support plain-text batch error bodies

* Tests: add Mistral Voxtral live transcription smoke

* Docs: add Mistral live audio test command

* Revert: remove Mistral live voice test and docs entry

* Onboard: re-export Mistral default model ref from models

* Changelog: credit joeVenner for Mistral work

* fix: include Mistral in auto audio key fallback

* Update CHANGELOG.md

* Update CHANGELOG.md

---------

Co-authored-by: Shakker <shakkerdroid@gmail.com>
This commit is contained in:
Vincent Koc
2026-02-22 19:03:56 -05:00
committed by GitHub
parent a66b98a9da
commit d92ba4f8aa
55 changed files with 996 additions and 66 deletions

View File

@@ -43,6 +43,7 @@ describe("buildAuthChoiceOptions", () => {
["Chutes OAuth auth choice", ["chutes"]],
["Qwen auth choice", ["qwen-portal"]],
["xAI auth choice", ["xai-api-key"]],
["Mistral auth choice", ["mistral-api-key"]],
["Volcano Engine auth choice", ["volcengine-api-key"]],
["BytePlus auth choice", ["byteplus-api-key"]],
["vLLM auth choice", ["vllm"]],

View File

@@ -70,6 +70,12 @@ const AUTH_CHOICE_GROUP_DEFS: {
hint: "API key",
choices: ["xai-api-key"],
},
{
value: "mistral",
label: "Mistral AI",
hint: "API key",
choices: ["mistral-api-key"],
},
{
value: "volcengine",
label: "Volcano Engine",
@@ -191,6 +197,7 @@ const BASE_AUTH_CHOICE_OPTIONS: ReadonlyArray<AuthChoiceOption> = [
hint: "Local/self-hosted OpenAI-compatible server",
},
{ value: "openai-api-key", label: "OpenAI API key" },
{ value: "mistral-api-key", label: "Mistral API key" },
{ value: "xai-api-key", label: "xAI (Grok) API key" },
{ value: "volcengine-api-key", label: "Volcano Engine API key" },
{ value: "byteplus-api-key", label: "BytePlus API key" },

View File

@@ -29,6 +29,8 @@ import {
applyKimiCodeProviderConfig,
applyLitellmConfig,
applyLitellmProviderConfig,
applyMistralConfig,
applyMistralProviderConfig,
applyMoonshotConfig,
applyMoonshotConfigCn,
applyMoonshotProviderConfig,
@@ -52,6 +54,7 @@ import {
QIANFAN_DEFAULT_MODEL_REF,
KIMI_CODING_MODEL_REF,
MOONSHOT_DEFAULT_MODEL_REF,
MISTRAL_DEFAULT_MODEL_REF,
SYNTHETIC_DEFAULT_MODEL_REF,
TOGETHER_DEFAULT_MODEL_REF,
VENICE_DEFAULT_MODEL_REF,
@@ -62,6 +65,7 @@ import {
setGeminiApiKey,
setLitellmApiKey,
setKimiCodingApiKey,
setMistralApiKey,
setMoonshotApiKey,
setOpencodeZenApiKey,
setSyntheticApiKey,
@@ -91,6 +95,7 @@ const API_KEY_TOKEN_PROVIDER_AUTH_CHOICE: Record<string, AuthChoice> = {
venice: "venice-api-key",
together: "together-api-key",
huggingface: "huggingface-api-key",
mistral: "mistral-api-key",
opencode: "opencode-zen",
qianfan: "qianfan-api-key",
};
@@ -190,6 +195,18 @@ const SIMPLE_API_KEY_PROVIDER_FLOWS: Partial<Record<AuthChoice, SimpleApiKeyProv
applyProviderConfig: applyXiaomiProviderConfig,
noteDefault: XIAOMI_DEFAULT_MODEL_REF,
},
"mistral-api-key": {
provider: "mistral",
profileId: "mistral:default",
expectedProviders: ["mistral"],
envLabel: "MISTRAL_API_KEY",
promptMessage: "Enter Mistral API key",
setCredential: setMistralApiKey,
defaultModel: MISTRAL_DEFAULT_MODEL_REF,
applyDefaultConfig: applyMistralConfig,
applyProviderConfig: applyMistralProviderConfig,
noteDefault: MISTRAL_DEFAULT_MODEL_REF,
},
"venice-api-key": {
provider: "venice",
profileId: "venice:default",

View File

@@ -20,6 +20,7 @@ const PREFERRED_PROVIDER_BY_AUTH_CHOICE: Partial<Record<AuthChoice, string>> = {
"gemini-api-key": "google",
"google-antigravity": "google-antigravity",
"google-gemini-cli": "google-gemini-cli",
"mistral-api-key": "mistral",
"zai-api-key": "zai",
"zai-coding-global": "zai",
"zai-coding-cn": "zai",

View File

@@ -66,6 +66,7 @@ describe("applyAuthChoice", () => {
"AI_GATEWAY_API_KEY",
"CLOUDFLARE_AI_GATEWAY_API_KEY",
"MOONSHOT_API_KEY",
"MISTRAL_API_KEY",
"KIMI_API_KEY",
"GEMINI_API_KEY",
"XIAOMI_API_KEY",
@@ -527,6 +528,13 @@ describe("applyAuthChoice", () => {
provider: "moonshot",
modelPrefix: "moonshot/",
},
{
authChoice: "mistral-api-key",
tokenProvider: "mistral",
profileId: "mistral:default",
provider: "mistral",
modelPrefix: "mistral/",
},
{
authChoice: "kimi-code-api-key",
tokenProvider: "kimi-code",
@@ -1267,6 +1275,10 @@ describe("resolvePreferredProviderForAuthChoice", () => {
expect(resolvePreferredProviderForAuthChoice("qwen-portal")).toBe("qwen-portal");
});
it("maps mistral-api-key to the provider", () => {
expect(resolvePreferredProviderForAuthChoice("mistral-api-key")).toBe("mistral");
});
it("returns undefined for unknown choices", () => {
expect(resolvePreferredProviderForAuthChoice("unknown" as AuthChoice)).toBeUndefined();
});

View File

@@ -104,6 +104,28 @@ describe("noteMemorySearchHealth", () => {
});
expect(note).not.toHaveBeenCalled();
});
it("resolves mistral auth for explicit mistral embedding provider", async () => {
resolveMemorySearchConfig.mockReturnValue({
provider: "mistral",
local: {},
remote: {},
});
resolveApiKeyForProvider.mockResolvedValue({
apiKey: "k",
source: "env: MISTRAL_API_KEY",
mode: "api-key",
});
await noteMemorySearchHealth(cfg);
expect(resolveApiKeyForProvider).toHaveBeenCalledWith({
provider: "mistral",
cfg,
agentDir: "/tmp/agent-default",
});
expect(note).not.toHaveBeenCalled();
});
});
describe("detectLegacyWorkspaceDirs", () => {

View File

@@ -76,7 +76,7 @@ export async function noteMemorySearchHealth(cfg: OpenClawConfig): Promise<void>
if (hasLocalEmbeddings(resolved.local)) {
return;
}
for (const provider of ["openai", "gemini", "voyage"] as const) {
for (const provider of ["openai", "gemini", "voyage", "mistral"] as const) {
if (hasRemoteApiKey || (await hasApiKeyForProvider(provider, cfg, agentDir))) {
return;
}
@@ -88,7 +88,7 @@ export async function noteMemorySearchHealth(cfg: OpenClawConfig): Promise<void>
"Semantic recall will not work without an embedding provider.",
"",
"Fix (pick one):",
"- Set OPENAI_API_KEY or GEMINI_API_KEY in your environment",
"- Set OPENAI_API_KEY, GEMINI_API_KEY, VOYAGE_API_KEY, or MISTRAL_API_KEY in your environment",
`- Add credentials: ${formatCliCommand("openclaw auth add --provider openai")}`,
`- For local embeddings: configure agents.defaults.memorySearch.provider and local model path`,
`- To disable: ${formatCliCommand("openclaw config set agents.defaults.memorySearch.enabled false")}`,
@@ -119,7 +119,7 @@ function hasLocalEmbeddings(local: { modelPath?: string }): boolean {
}
async function hasApiKeyForProvider(
provider: "openai" | "gemini" | "voyage",
provider: "openai" | "gemini" | "voyage" | "mistral",
cfg: OpenClawConfig,
agentDir: string,
): Promise<boolean> {

View File

@@ -31,6 +31,7 @@ import type { OpenClawConfig } from "../config/config.js";
import type { ModelApi } from "../config/types.models.js";
import {
HUGGINGFACE_DEFAULT_MODEL_REF,
MISTRAL_DEFAULT_MODEL_REF,
OPENROUTER_DEFAULT_MODEL_REF,
TOGETHER_DEFAULT_MODEL_REF,
XIAOMI_DEFAULT_MODEL_REF,
@@ -57,9 +58,12 @@ import {
applyProviderConfigWithModelCatalog,
} from "./onboard-auth.config-shared.js";
import {
buildMistralModelDefinition,
buildZaiModelDefinition,
buildMoonshotModelDefinition,
buildXaiModelDefinition,
MISTRAL_BASE_URL,
MISTRAL_DEFAULT_MODEL_ID,
QIANFAN_BASE_URL,
QIANFAN_DEFAULT_MODEL_REF,
KIMI_CODING_MODEL_ID,
@@ -402,6 +406,30 @@ export function applyXaiConfig(cfg: OpenClawConfig): OpenClawConfig {
return applyAgentDefaultModelPrimary(next, XAI_DEFAULT_MODEL_REF);
}
export function applyMistralProviderConfig(cfg: OpenClawConfig): OpenClawConfig {
const models = { ...cfg.agents?.defaults?.models };
models[MISTRAL_DEFAULT_MODEL_REF] = {
...models[MISTRAL_DEFAULT_MODEL_REF],
alias: models[MISTRAL_DEFAULT_MODEL_REF]?.alias ?? "Mistral",
};
const defaultModel = buildMistralModelDefinition();
return applyProviderConfigWithDefaultModel(cfg, {
agentModels: models,
providerId: "mistral",
api: "openai-completions",
baseUrl: MISTRAL_BASE_URL,
defaultModel,
defaultModelId: MISTRAL_DEFAULT_MODEL_ID,
});
}
export function applyMistralConfig(cfg: OpenClawConfig): OpenClawConfig {
const next = applyMistralProviderConfig(cfg);
return applyAgentDefaultModelPrimary(next, MISTRAL_DEFAULT_MODEL_REF);
}
export function applyAuthProfileConfig(
cfg: OpenClawConfig,
params: {

View File

@@ -5,7 +5,7 @@ import { resolveOpenClawAgentDir } from "../agents/agent-paths.js";
import { upsertAuthProfile } from "../agents/auth-profiles.js";
import { resolveStateDir } from "../config/paths.js";
export { CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_REF } from "../agents/cloudflare-ai-gateway.js";
export { XAI_DEFAULT_MODEL_REF } from "./onboard-auth.models.js";
export { MISTRAL_DEFAULT_MODEL_REF, XAI_DEFAULT_MODEL_REF } from "./onboard-auth.models.js";
const resolveAuthAgentDir = (agentDir?: string) => agentDir ?? resolveOpenClawAgentDir();
@@ -360,3 +360,15 @@ export function setXaiApiKey(key: string, agentDir?: string) {
agentDir: resolveAuthAgentDir(agentDir),
});
}
export async function setMistralApiKey(key: string, agentDir?: string) {
upsertAuthProfile({
profileId: "mistral:default",
credential: {
type: "api_key",
provider: "mistral",
key,
},
agentDir: resolveAuthAgentDir(agentDir),
});
}

View File

@@ -137,6 +137,30 @@ export function buildMoonshotModelDefinition(): ModelDefinitionConfig {
};
}
export const MISTRAL_BASE_URL = "https://api.mistral.ai/v1";
export const MISTRAL_DEFAULT_MODEL_ID = "mistral-large-latest";
export const MISTRAL_DEFAULT_MODEL_REF = `mistral/${MISTRAL_DEFAULT_MODEL_ID}`;
export const MISTRAL_DEFAULT_CONTEXT_WINDOW = 262144;
export const MISTRAL_DEFAULT_MAX_TOKENS = 262144;
export const MISTRAL_DEFAULT_COST = {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
};
export function buildMistralModelDefinition(): ModelDefinitionConfig {
return {
id: MISTRAL_DEFAULT_MODEL_ID,
name: "Mistral Large",
reasoning: false,
input: ["text", "image"],
cost: MISTRAL_DEFAULT_COST,
contextWindow: MISTRAL_DEFAULT_CONTEXT_WINDOW,
maxTokens: MISTRAL_DEFAULT_MAX_TOKENS,
};
}
export function buildZaiModelDefinition(params: {
id: string;
name?: string;

View File

@@ -7,6 +7,8 @@ import type { OpenClawConfig } from "../config/config.js";
import {
applyAuthProfileConfig,
applyLitellmProviderConfig,
applyMistralConfig,
applyMistralProviderConfig,
applyMinimaxApiConfig,
applyMinimaxApiProviderConfig,
applyOpencodeZenConfig,
@@ -22,6 +24,7 @@ import {
applyZaiConfig,
applyZaiProviderConfig,
OPENROUTER_DEFAULT_MODEL_REF,
MISTRAL_DEFAULT_MODEL_REF,
SYNTHETIC_DEFAULT_MODEL_ID,
SYNTHETIC_DEFAULT_MODEL_REF,
XAI_DEFAULT_MODEL_REF,
@@ -540,9 +543,46 @@ describe("applyXaiProviderConfig", () => {
});
});
describe("applyMistralConfig", () => {
it("adds Mistral provider with correct settings", () => {
const cfg = applyMistralConfig({});
expect(cfg.models?.providers?.mistral).toMatchObject({
baseUrl: "https://api.mistral.ai/v1",
api: "openai-completions",
});
expect(cfg.agents?.defaults?.model?.primary).toBe(MISTRAL_DEFAULT_MODEL_REF);
});
});
describe("applyMistralProviderConfig", () => {
it("merges Mistral models and keeps existing provider overrides", () => {
const cfg = applyMistralProviderConfig(
createLegacyProviderConfig({
providerId: "mistral",
api: "anthropic-messages",
modelId: "custom-model",
modelName: "Custom",
}),
);
expect(cfg.models?.providers?.mistral?.baseUrl).toBe("https://api.mistral.ai/v1");
expect(cfg.models?.providers?.mistral?.api).toBe("openai-completions");
expect(cfg.models?.providers?.mistral?.apiKey).toBe("old-key");
expect(cfg.models?.providers?.mistral?.models.map((m) => m.id)).toEqual([
"custom-model",
"mistral-large-latest",
]);
const mistralDefault = cfg.models?.providers?.mistral?.models.find(
(model) => model.id === "mistral-large-latest",
);
expect(mistralDefault?.contextWindow).toBe(262144);
expect(mistralDefault?.maxTokens).toBe(262144);
});
});
describe("fallback preservation helpers", () => {
it("preserves existing model fallbacks", () => {
const fallbackCases = [applyMinimaxApiConfig, applyXaiConfig] as const;
const fallbackCases = [applyMinimaxApiConfig, applyXaiConfig, applyMistralConfig] as const;
for (const applyConfig of fallbackCases) {
const cfg = applyConfig(createConfigWithFallbacks());
expectFallbacksPreserved(cfg);
@@ -563,6 +603,11 @@ describe("provider alias defaults", () => {
modelRef: XAI_DEFAULT_MODEL_REF,
alias: "Grok",
},
{
applyConfig: () => applyMistralProviderConfig({}),
modelRef: MISTRAL_DEFAULT_MODEL_REF,
alias: "Mistral",
},
] as const;
for (const testCase of aliasCases) {
const cfg = testCase.applyConfig();

View File

@@ -15,6 +15,8 @@ export {
applyKimiCodeProviderConfig,
applyLitellmConfig,
applyLitellmProviderConfig,
applyMistralConfig,
applyMistralProviderConfig,
applyMoonshotConfig,
applyMoonshotConfigCn,
applyMoonshotProviderConfig,
@@ -62,6 +64,7 @@ export {
setLitellmApiKey,
setKimiCodingApiKey,
setMinimaxApiKey,
setMistralApiKey,
setMoonshotApiKey,
setOpencodeZenApiKey,
setOpenrouterApiKey,
@@ -79,11 +82,13 @@ export {
XIAOMI_DEFAULT_MODEL_REF,
ZAI_DEFAULT_MODEL_REF,
TOGETHER_DEFAULT_MODEL_REF,
MISTRAL_DEFAULT_MODEL_REF,
XAI_DEFAULT_MODEL_REF,
} from "./onboard-auth.credentials.js";
export {
buildMinimaxApiModelDefinition,
buildMinimaxModelDefinition,
buildMistralModelDefinition,
buildMoonshotModelDefinition,
buildZaiModelDefinition,
DEFAULT_MINIMAX_BASE_URL,
@@ -100,6 +105,8 @@ export {
MOONSHOT_BASE_URL,
MOONSHOT_DEFAULT_MODEL_ID,
MOONSHOT_DEFAULT_MODEL_REF,
MISTRAL_BASE_URL,
MISTRAL_DEFAULT_MODEL_ID,
resolveZaiBaseUrl,
ZAI_CODING_CN_BASE_URL,
ZAI_DEFAULT_MODEL_ID,

View File

@@ -253,6 +253,23 @@ describe("onboard (non-interactive): provider auth", () => {
});
}, 60_000);
it("infers Mistral auth choice from --mistral-api-key and sets default model", async () => {
await withOnboardEnv("openclaw-onboard-mistral-infer-", async (env) => {
const cfg = await runOnboardingAndReadConfig(env, {
mistralApiKey: "mistral-test-key",
});
expect(cfg.auth?.profiles?.["mistral:default"]?.provider).toBe("mistral");
expect(cfg.auth?.profiles?.["mistral:default"]?.mode).toBe("api_key");
expect(cfg.agents?.defaults?.model?.primary).toBe("mistral/mistral-large-latest");
await expectApiKeyProfile({
profileId: "mistral:default",
provider: "mistral",
key: "mistral-test-key",
});
});
}, 60_000);
it("stores Volcano Engine API key and sets default model", async () => {
await withOnboardEnv("openclaw-onboard-volcengine-", async (env) => {
const cfg = await runOnboardingAndReadConfig(env, {

View File

@@ -12,6 +12,7 @@ type AuthChoiceFlagOptions = Pick<
| "anthropicApiKey"
| "geminiApiKey"
| "openaiApiKey"
| "mistralApiKey"
| "openrouterApiKey"
| "aiGatewayApiKey"
| "cloudflareAiGatewayApiKey"

View File

@@ -27,6 +27,7 @@ import {
applyHuggingfaceConfig,
applyVercelAiGatewayConfig,
applyLitellmConfig,
applyMistralConfig,
applyXaiConfig,
applyXiaomiConfig,
applyZaiConfig,
@@ -36,6 +37,7 @@ import {
setGeminiApiKey,
setKimiCodingApiKey,
setLitellmApiKey,
setMistralApiKey,
setMinimaxApiKey,
setMoonshotApiKey,
setOpencodeZenApiKey,
@@ -304,6 +306,29 @@ export async function applyNonInteractiveAuthChoice(params: {
return applyXaiConfig(nextConfig);
}
if (authChoice === "mistral-api-key") {
const resolved = await resolveNonInteractiveApiKey({
provider: "mistral",
cfg: baseConfig,
flagValue: opts.mistralApiKey,
flagName: "--mistral-api-key",
envVar: "MISTRAL_API_KEY",
runtime,
});
if (!resolved) {
return null;
}
if (resolved.source !== "profile") {
await setMistralApiKey(resolved.key);
}
nextConfig = applyAuthProfileConfig(nextConfig, {
profileId: "mistral:default",
provider: "mistral",
mode: "api_key",
});
return applyMistralConfig(nextConfig);
}
if (authChoice === "volcengine-api-key") {
const resolved = await resolveNonInteractiveApiKey({
provider: "volcengine",

View File

@@ -4,6 +4,7 @@ type OnboardProviderAuthOptionKey = keyof Pick<
OnboardOptions,
| "anthropicApiKey"
| "openaiApiKey"
| "mistralApiKey"
| "openrouterApiKey"
| "aiGatewayApiKey"
| "cloudflareAiGatewayApiKey"
@@ -49,6 +50,13 @@ export const ONBOARD_PROVIDER_AUTH_FLAGS: ReadonlyArray<OnboardProviderAuthFlag>
cliOption: "--openai-api-key <key>",
description: "OpenAI API key",
},
{
optionKey: "mistralApiKey",
authChoice: "mistral-api-key",
cliFlag: "--mistral-api-key",
cliOption: "--mistral-api-key <key>",
description: "Mistral API key",
},
{
optionKey: "openrouterApiKey",
authChoice: "openrouter-api-key",

View File

@@ -45,6 +45,7 @@ export type AuthChoice =
| "copilot-proxy"
| "qwen-portal"
| "xai-api-key"
| "mistral-api-key"
| "volcengine-api-key"
| "byteplus-api-key"
| "qianfan-api-key"
@@ -68,6 +69,7 @@ export type AuthChoiceGroupId =
| "minimax"
| "synthetic"
| "venice"
| "mistral"
| "qwen"
| "together"
| "huggingface"
@@ -105,6 +107,7 @@ export type OnboardOptions = {
tokenExpiresIn?: string;
anthropicApiKey?: string;
openaiApiKey?: string;
mistralApiKey?: string;
openrouterApiKey?: string;
litellmApiKey?: string;
aiGatewayApiKey?: string;