diff --git a/src/background/index.mjs b/src/background/index.mjs index 89a1ea048..3bfb39da4 100644 --- a/src/background/index.mjs +++ b/src/background/index.mjs @@ -26,6 +26,7 @@ import { isUsingGeminiWebModel, isUsingClaudeWebModel, isUsingMoonshotApiModel, + isUsingMistralApiModel, isUsingMoonshotWebModel, isUsingOpenRouterApiModel, isUsingAimlApiModel, @@ -396,6 +397,7 @@ function isUsingOpenAICompatibleApiSession(session) { isUsingCustomModel(session) || isUsingChatgptApiModel(session) || isUsingMoonshotApiModel(session) || + isUsingMistralApiModel(session) || isUsingChatGLMApiModel(session) || isUsingDeepSeekApiModel(session) || isUsingOllamaApiModel(session) || diff --git a/src/config/index.mjs b/src/config/index.mjs index e1a22f822..d2b09516d 100644 --- a/src/config/index.mjs +++ b/src/config/index.mjs @@ -123,6 +123,11 @@ export const moonshotApiModelKeys = [ 'moonshot_v1_32k', 'moonshot_v1_128k', ] +export const mistralApiModelKeys = [ + 'mistralMediumLatest', + 'mistralSmallLatest', + 'mistralLargeLatest', +] export const deepSeekApiModelKeys = [ 'deepseek_chat', 'deepseek_reasoner', @@ -229,6 +234,10 @@ export const ModelGroups = { value: moonshotApiModelKeys, desc: 'Kimi.Moonshot (API)', }, + mistralApiModelKeys: { + value: mistralApiModelKeys, + desc: 'Mistral AI (API)', + }, chatglmApiModelKeys: { value: chatglmApiModelKeys, desc: 'ChatGLM (API)', @@ -433,6 +442,19 @@ export const Models = { desc: 'DeepSeek (Reasoner)', }, + mistralMediumLatest: { + value: 'mistral-medium-latest', + desc: 'Mistral AI (Medium latest)', + }, + mistralSmallLatest: { + value: 'mistral-small-latest', + desc: 'Mistral AI (Small latest)', + }, + mistralLargeLatest: { + value: 'mistral-large-latest', + desc: 'Mistral AI (Large latest)', + }, + openRouter_anthropic_claude_haiku4_5: { value: 'anthropic/claude-haiku-4.5', desc: 'OpenRouter (Claude Haiku 4.5)', @@ -691,6 +713,7 @@ export const defaultConfig = { anthropicApiKey: '', chatglmApiKey: '', moonshotApiKey: '', + mistralApiKey: '', deepSeekApiKey: '', customApiKey: '', @@ -750,6 +773,7 @@ export const defaultConfig = { 'claudeHaiku45Api', 'googleGemini3_1Pro', 'googleGemini3_5Flash', + 'mistralMediumLatest', 'openRouter_auto', 'openRouter_free', ], @@ -895,6 +919,10 @@ export function isUsingMoonshotApiModel(configOrSession) { return isInApiModeGroup(moonshotApiModelKeys, configOrSession) } +export function isUsingMistralApiModel(configOrSession) { + return isInApiModeGroup(mistralApiModelKeys, configOrSession) +} + export function isUsingDeepSeekApiModel(configOrSession) { return isInApiModeGroup(deepSeekApiModelKeys, configOrSession) } diff --git a/src/config/openai-provider-mappings.mjs b/src/config/openai-provider-mappings.mjs index 57c0485a3..1e2f7e7c2 100644 --- a/src/config/openai-provider-mappings.mjs +++ b/src/config/openai-provider-mappings.mjs @@ -2,6 +2,7 @@ export const LEGACY_API_KEY_FIELD_BY_PROVIDER_ID = { openai: 'apiKey', deepseek: 'deepSeekApiKey', moonshot: 'moonshotApiKey', + mistral: 'mistralApiKey', openrouter: 'openRouterApiKey', aiml: 'aimlApiKey', chatglm: 'chatglmApiKey', @@ -21,6 +22,7 @@ export const OPENAI_COMPATIBLE_GROUP_TO_PROVIDER_ID = { chatgptApiModelKeys: 'openai', gptApiModelKeys: 'openai', moonshotApiModelKeys: 'moonshot', + mistralApiModelKeys: 'mistral', deepSeekApiModelKeys: 'deepseek', openRouterApiModelKeys: 'openrouter', aimlModelKeys: 'aiml', diff --git a/src/popup/sections/GeneralPart.jsx b/src/popup/sections/GeneralPart.jsx index bf1798b89..066ffb156 100644 --- a/src/popup/sections/GeneralPart.jsx +++ b/src/popup/sections/GeneralPart.jsx @@ -62,6 +62,8 @@ function getProviderApiKeySetupUrl(providerId) { return 'https://platform.openai.com/account/api-keys' case 'moonshot': return 'https://platform.moonshot.cn/console/api-keys' + case 'mistral': + return 'https://console.mistral.ai/api-keys' default: return '' } diff --git a/src/services/apis/provider-registry.mjs b/src/services/apis/provider-registry.mjs index 72aa47662..c0e18403f 100644 --- a/src/services/apis/provider-registry.mjs +++ b/src/services/apis/provider-registry.mjs @@ -35,6 +35,14 @@ const BUILTIN_PROVIDER_TEMPLATE = [ builtin: true, enabled: true, }, + { + id: 'mistral', + name: 'Mistral AI', + baseUrl: 'https://api.mistral.ai/v1', + chatCompletionsPath: '/chat/completions', + builtin: true, + enabled: true, + }, { id: 'openrouter', name: 'OpenRouter', @@ -111,6 +119,8 @@ function resolveProviderIdFromLegacyModelName(modelName) { } if (preset.startsWith('deepseek_') || preset === 'deepSeekApiModelKeys') return 'deepseek' if (preset.startsWith('moonshot_') || preset === 'moonshotApiModelKeys') return 'moonshot' + if (/^mistral(?:Medium|Small|Large)/.test(preset) || preset === 'mistralApiModelKeys') + return 'mistral' if (preset.startsWith('openRouter_') || preset === 'openRouterApiModelKeys') return 'openrouter' if (preset.startsWith('aiml_') || preset === 'aimlModelKeys' || preset === 'aimlApiModelKeys') { return 'aiml' diff --git a/tests/unit/config/config-predicates.test.mjs b/tests/unit/config/config-predicates.test.mjs index a899f31df..6aff7c722 100644 --- a/tests/unit/config/config-predicates.test.mjs +++ b/tests/unit/config/config-predicates.test.mjs @@ -7,6 +7,7 @@ import { chatgptApiModelKeys, gptApiModelKeys, claudeApiModelKeys, + mistralApiModelKeys, openRouterApiModelKeys, aimlApiModelKeys, isUsingAimlApiModel, @@ -22,6 +23,7 @@ import { isUsingGeminiWebModel, isUsingGithubThirdPartyApiModel, isUsingMoonshotApiModel, + isUsingMistralApiModel, isUsingMoonshotWebModel, isUsingMultiModeModel, isUsingOllamaApiModel, @@ -223,6 +225,13 @@ test('isUsingMoonshotApiModel detects moonshot API models', () => { assert.equal(isUsingMoonshotApiModel({ modelName: 'moonshotWebFree' }), false) }) +test('isUsingMistralApiModel accepts exported Mistral API model keys', () => { + for (const modelName of mistralApiModelKeys) { + assert.equal(isUsingMistralApiModel({ modelName }), true) + } + assert.equal(isUsingMistralApiModel({ modelName: 'chatgptApi4oMini' }), false) +}) + test('isUsingDeepSeekApiModel detects DeepSeek models', () => { assert.equal(isUsingDeepSeekApiModel({ modelName: 'deepseek_v4_flash' }), true) assert.equal(isUsingDeepSeekApiModel({ modelName: 'deepseek_v4_pro' }), true) diff --git a/tests/unit/services/apis/provider-registry.test.mjs b/tests/unit/services/apis/provider-registry.test.mjs index 29d949814..399834447 100644 --- a/tests/unit/services/apis/provider-registry.test.mjs +++ b/tests/unit/services/apis/provider-registry.test.mjs @@ -41,6 +41,11 @@ test('resolveEndpointTypeForSession falls back to legacy modelName when apiMode assert.equal(resolveEndpointTypeForSession(session), 'completion') }) +test('resolveProviderIdForSession resolves legacy Mistral preset keys', () => { + assert.equal(resolveProviderIdForSession({ modelName: 'mistralMediumLatest' }), 'mistral') + assert.equal(resolveProviderIdForSession({ modelName: 'mistralApiModelKeys-custom' }), 'mistral') +}) + test('resolveProviderIdForSession resolves legacy Google preset keys', () => { assert.equal(resolveProviderIdForSession({ modelName: 'googleGemini2_5Flash' }), 'google') assert.equal(resolveProviderIdForSession({ modelName: 'googleApiModelKeys-custom' }), 'google') diff --git a/tests/unit/services/apis/thin-adapters.test.mjs b/tests/unit/services/apis/thin-adapters.test.mjs index 385fd25f7..40c0c3e9f 100644 --- a/tests/unit/services/apis/thin-adapters.test.mjs +++ b/tests/unit/services/apis/thin-adapters.test.mjs @@ -49,6 +49,13 @@ const adapters = [ expectedBaseUrl: 'https://api.moonshot.cn/v1', expectedApiKey: 'ms-key', }, + { + name: 'mistral-api', + apiMode: { groupName: 'mistralApiModelKeys', itemName: 'mistralMediumLatest' }, + providerId: 'mistral', + expectedBaseUrl: 'https://api.mistral.ai/v1', + expectedApiKey: 'mistral-key', + }, { name: 'openrouter-api', apiMode: { groupName: 'openRouterApiModelKeys', itemName: 'openRouter_openai_o3' },