Decouple and fix mistral (#595)

* decouple and fix mistral

* fix wrong variable for currentBaseUrl and buildAPIProviderProperties
This commit is contained in:
lunamonke
2026-04-12 08:26:14 +01:00
committed by GitHub
parent b126e38b1a
commit 4c50977f3c
24 changed files with 556 additions and 46 deletions

View File

@@ -1,20 +1,44 @@
import { afterEach, expect, mock, test } from 'bun:test'
import { getAdditionalModelOptionsCacheScope } from '../../services/api/providerConfig.js'
import { getAPIProvider } from '../../utils/model/providers.js'
const originalEnv = {
CLAUDE_CODE_USE_OPENAI: process.env.CLAUDE_CODE_USE_OPENAI,
CLAUDE_CODE_USE_GEMINI: process.env.CLAUDE_CODE_USE_GEMINI,
CLAUDE_CODE_USE_GITHUB: process.env.CLAUDE_CODE_USE_GITHUB,
CLAUDE_CODE_USE_MISTRAL: process.env.CLAUDE_CODE_USE_MISTRAL,
CLAUDE_CODE_USE_BEDROCK: process.env.CLAUDE_CODE_USE_BEDROCK,
CLAUDE_CODE_USE_VERTEX: process.env.CLAUDE_CODE_USE_VERTEX,
CLAUDE_CODE_USE_FOUNDRY: process.env.CLAUDE_CODE_USE_FOUNDRY,
OPENAI_BASE_URL: process.env.OPENAI_BASE_URL,
OPENAI_API_BASE: process.env.OPENAI_API_BASE,
OPENAI_MODEL: process.env.OPENAI_MODEL,
}
afterEach(() => {
mock.restore()
process.env.CLAUDE_CODE_USE_OPENAI = originalEnv.CLAUDE_CODE_USE_OPENAI
process.env.CLAUDE_CODE_USE_GEMINI = originalEnv.CLAUDE_CODE_USE_GEMINI
process.env.CLAUDE_CODE_USE_GITHUB = originalEnv.CLAUDE_CODE_USE_GITHUB
process.env.CLAUDE_CODE_USE_MISTRAL = originalEnv.CLAUDE_CODE_USE_MISTRAL
process.env.CLAUDE_CODE_USE_BEDROCK = originalEnv.CLAUDE_CODE_USE_BEDROCK
process.env.CLAUDE_CODE_USE_VERTEX = originalEnv.CLAUDE_CODE_USE_VERTEX
process.env.CLAUDE_CODE_USE_FOUNDRY = originalEnv.CLAUDE_CODE_USE_FOUNDRY
process.env.OPENAI_BASE_URL = originalEnv.OPENAI_BASE_URL
process.env.OPENAI_API_BASE = originalEnv.OPENAI_API_BASE
process.env.OPENAI_MODEL = originalEnv.OPENAI_MODEL
})
test('opens the model picker without awaiting local model discovery refresh', async () => {
process.env.CLAUDE_CODE_USE_OPENAI = '1'
delete process.env.CLAUDE_CODE_USE_GEMINI
delete process.env.CLAUDE_CODE_USE_GITHUB
delete process.env.CLAUDE_CODE_USE_MISTRAL
delete process.env.CLAUDE_CODE_USE_BEDROCK
delete process.env.CLAUDE_CODE_USE_VERTEX
delete process.env.CLAUDE_CODE_USE_FOUNDRY
delete process.env.OPENAI_API_BASE
process.env.OPENAI_BASE_URL = 'http://127.0.0.1:8080/v1'
process.env.OPENAI_MODEL = 'qwen2.5-coder-7b-instruct'
@@ -30,7 +54,9 @@ test('opens the model picker without awaiting local model discovery refresh', as
discoverOpenAICompatibleModelOptions,
}))
const { call } = await import(`./model.js?ts=${Date.now()}-${Math.random()}`)
expect(getAdditionalModelOptionsCacheScope()).toBe('openai:http://127.0.0.1:8080/v1')
const { call } = await import('./model.js')
const result = await Promise.race([
call(() => {}, {} as never, ''),
new Promise(resolve => setTimeout(() => resolve('timeout'), 50)),

View File

@@ -284,7 +284,7 @@ function haveSameModelOptions(left: ModelOption[], right: ModelOption[]): boolea
});
}
async function refreshOpenAIModelOptionsCache(): Promise<void> {
if (getAPIProvider() !== 'openai') {
if (!getAdditionalModelOptionsCacheScope()?.startsWith('openai:')) {
return;
}
try {

View File

@@ -22,11 +22,14 @@ import {
import {
buildCodexProfileEnv,
buildGeminiProfileEnv,
buildMistralProfileEnv,
buildOllamaProfileEnv,
buildOpenAIProfileEnv,
createProfileFile,
DEFAULT_GEMINI_BASE_URL,
DEFAULT_GEMINI_MODEL,
DEFAULT_MISTRAL_BASE_URL,
DEFAULT_MISTRAL_MODEL,
deleteProfileFile,
loadProfileFile,
maskSecretForDisplay,
@@ -74,6 +77,14 @@ type Step =
baseUrl: string | null
defaultModel: string
}
| { name: 'mistral-key'; defaultModel: string }
| { name: 'mistral-base'; apiKey: string; defaultModel: string }
| {
name: 'mistral-model'
apiKey: string
baseUrl: string | null
defaultModel: string
}
| { name: 'gemini-auth-method' }
| { name: 'gemini-key' }
| { name: 'gemini-access-token' }
@@ -116,6 +127,8 @@ type ProviderWizardDefaults = {
openAIModel: string
openAIBaseUrl: string
geminiModel: string
mistralModel: string
mistralBaseUrl: string
}
function isEnvTruthy(value: string | undefined): boolean {
@@ -147,11 +160,19 @@ export function getProviderWizardDefaults(
const safeGeminiModel =
sanitizeProviderConfigValue(processEnv.GEMINI_MODEL, processEnv) ||
DEFAULT_GEMINI_MODEL
const safeMistralModel =
sanitizeProviderConfigValue(processEnv.MISTRAL_MODEL, processEnv) ||
DEFAULT_MISTRAL_MODEL
const safeMistralBaseUrl =
sanitizeProviderConfigValue(processEnv.MISTRAL_BASE_URL, processEnv) ||
DEFAULT_MISTRAL_BASE_URL
return {
openAIModel: safeOpenAIModel,
openAIBaseUrl: safeOpenAIBaseUrl,
geminiModel: safeGeminiModel,
mistralModel: safeMistralModel,
mistralBaseUrl: safeMistralBaseUrl,
}
}
@@ -178,6 +199,21 @@ export function buildCurrentProviderSummary(options?: {
}
}
if (isEnvTruthy(processEnv.CLAUDE_CODE_USE_MISTRAL)) {
return {
providerLabel: 'Mistral',
modelLabel: getSafeDisplayValue(
processEnv.MISTRAL_MODEL ?? DEFAULT_MISTRAL_MODEL,
processEnv
),
endpointLabel: getSafeDisplayValue(
processEnv.MISTRAL_BASE_URL ?? DEFAULT_MISTRAL_BASE_URL,
processEnv
),
savedProfileLabel,
}
}
if (isEnvTruthy(processEnv.CLAUDE_CODE_USE_GITHUB)) {
return {
providerLabel: 'GitHub Models',
@@ -259,6 +295,24 @@ function buildSavedProfileSummary(
? 'configured'
: undefined,
}
case 'mistral':
return {
providerLabel: 'Mistral',
modelLabel: getSafeDisplayValue(
env.MISTRAL_MODEL ?? DEFAULT_MISTRAL_MODEL,
process.env,
env,
),
endpointLabel: getSafeDisplayValue(
env.MISTRAL_BASE_URL ?? DEFAULT_MISTRAL_BASE_URL,
process.env,
env,
),
credentialLabel:
maskSecretForDisplay(env.MISTRAL_API_KEY) !== undefined
? 'configured'
: undefined,
}
case 'codex':
return {
providerLabel: 'Codex',
@@ -473,6 +527,11 @@ function ProviderChooser({
value: 'gemini',
description: 'Use Google Gemini with API key, access token, or local ADC',
},
{
label: 'Mistral',
value: 'mistral',
description: 'Use Mistral with API key'
},
{
label: 'Codex',
value: 'codex',
@@ -971,6 +1030,11 @@ export function ProviderWizard({
})
} else if (value === 'gemini') {
setStep({ name: 'gemini-auth-method' })
} else if (value === 'mistral') {
setStep({
name: 'mistral-key',
defaultModel: defaults.mistralModel,
})
} else if (value === 'clear') {
const filePath = deleteProfileFile()
onDone(`Removed saved provider profile at ${filePath}. Restart OpenClaude to go back to normal startup.`, {
@@ -1110,6 +1174,101 @@ export function ProviderWizard({
/>
)
case 'mistral-key':
return (
<TextEntryDialog
resetStateKey={step.name}
title="Mistral setup"
subtitle="Step 1 of 3"
description={
process.env.MISTRAL_API_KEY
? 'Enter an API key, or leave this blank to reuse the current MISTRAL_API_KEY from this session.'
: 'Enter the API key for your Mistral provider.'
}
initialValue=""
placeholder="..."
mask="*"
allowEmpty={Boolean(process.env.MISTRAL_API_KEY)}
validate={value => {
const candidate = value.trim() || process.env.MISTRAL_API_KEY || ''
return sanitizeApiKey(candidate)
? null
: 'Enter a real API key. Placeholder values like SUA_CHAVE are not valid.'
}}
onSubmit={value => {
const apiKey = value.trim() || process.env.MISTRAL_API_KEY || ''
setStep({
name: 'mistral-base',
apiKey,
defaultModel: step.defaultModel,
})
}}
onCancel={() => setStep({ name: 'choose' })}
/>
)
case 'mistral-base':
return (
<TextEntryDialog
resetStateKey={step.name}
title="Mistral setup"
subtitle="Step 2 of 3"
description={`Optionally enter a base URL. Leave blank for ${DEFAULT_MISTRAL_BASE_URL}.`}
initialValue={
defaults.mistralBaseUrl === DEFAULT_MISTRAL_BASE_URL
? ''
: defaults.mistralBaseUrl
}
placeholder={DEFAULT_MISTRAL_BASE_URL}
allowEmpty
onSubmit={value => {
setStep({
name: 'mistral-model',
apiKey: step.apiKey,
baseUrl: value.trim() || null,
defaultModel: step.defaultModel,
})
}}
onCancel={() =>
setStep({
name: 'mistral-key',
defaultModel: step.defaultModel,
})
}
/>
)
case 'mistral-model':
return (
<TextEntryDialog
resetStateKey={step.name}
title="Mistral setup"
subtitle="Step 3 of 3"
description={`Enter a model name. Leave blank for ${step.defaultModel}.`}
initialValue={defaults.mistralModel ?? step.defaultModel}
placeholder={step.defaultModel}
allowEmpty
onSubmit={value => {
const env = buildMistralProfileEnv({
model: value.trim() || step.defaultModel,
baseUrl: step.baseUrl,
apiKey: step.apiKey,
processEnv: process.env,
})
if (env) {
finishProfileSave(onDone, 'mistral', env)
}
}}
onCancel={() =>
setStep({
name: 'mistral-base',
apiKey: step.apiKey,
defaultModel: step.defaultModel,
})
}
/>
)
case 'gemini-auth-method': {
const hasShellGeminiKey = Boolean(
process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY,