Skip to content

Commit 55c898f

Browse files
committed
decouple and fix mistral
1 parent f4ac709 commit 55c898f

24 files changed

Lines changed: 556 additions & 46 deletions

docs/advanced-setup.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -137,10 +137,9 @@ export OPENAI_MODEL=llama-3.3-70b-versatile
137137
### Mistral
138138

139139
```bash
140-
export CLAUDE_CODE_USE_OPENAI=1
141-
export OPENAI_API_KEY=...
142-
export OPENAI_BASE_URL=https://api.mistral.ai/v1
143-
export OPENAI_MODEL=mistral-large-latest
140+
export CLAUDE_CODE_USE_MISTRAL=1
141+
export MISTRAL_API_KEY=...
142+
export MISTRAL_MODEL=mistral-large-latest
144143
```
145144

146145
### Azure OpenAI

python/smart_router.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,14 @@ def build_default_providers() -> list[Provider]:
112112
big_model=big if "gemini" in big else "gemini-2.5-pro",
113113
small_model=small if "gemini" in small else "gemini-2.0-flash",
114114
),
115+
Provider(
116+
name="mistral",
117+
ping_url="",
118+
api_key_env="MISTRAL_API_KEY",
119+
cost_per_1k_tokens=0.0001,
120+
big_model=big if "mistral" in big else "devstral-latest",
121+
small_model=small if "small" in small else "ministral-3b-latest",
122+
),
115123
Provider(
116124
name="ollama",
117125
ping_url=f"{ollama_url}/api/tags",

scripts/provider-bootstrap.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
buildAtomicChatProfileEnv,
1212
buildCodexProfileEnv,
1313
buildGeminiProfileEnv,
14+
buildMistralProfileEnv,
1415
buildOllamaProfileEnv,
1516
buildOpenAIProfileEnv,
1617
createProfileFile,
@@ -37,7 +38,7 @@ function parseArg(name: string): string | null {
3738

3839
function parseProviderArg(): ProviderProfile | 'auto' {
3940
const p = parseArg('--provider')?.toLowerCase()
40-
if (p === 'openai' || p === 'ollama' || p === 'codex' || p === 'gemini' || p === 'atomic-chat') return p
41+
if (p === 'openai' || p === 'ollama' || p === 'codex' || p === 'gemini' || p === 'mistral' || p === 'atomic-chat') return p
4142
return 'auto'
4243
}
4344

@@ -90,6 +91,21 @@ async function main(): Promise<void> {
9091
process.exit(1)
9192
}
9293

94+
env = builtEnv
95+
} else if (selected === 'mistral') {
96+
const builtEnv = buildMistralProfileEnv({
97+
model: argModel || null,
98+
baseUrl: argBaseUrl || null,
99+
apiKey: argApiKey || null,
100+
processEnv: process.env,
101+
})
102+
103+
if (!builtEnv) {
104+
console.error('Mistral profile requires an API key. Use --api-key or set MISTRAL_API_KEY.')
105+
console.error('Get a free key at: https://admin.mistral.ai/organization/api-keys')
106+
process.exit(1)
107+
}
108+
93109
env = builtEnv
94110
} else if (selected === 'ollama') {
95111
resolvedOllamaModel ??= await resolveOllamaModel(argModel, argBaseUrl, goal)
@@ -169,7 +185,7 @@ async function main(): Promise<void> {
169185

170186
console.log(`Saved profile: ${selected}`)
171187
console.log(`Goal: ${goal}`)
172-
console.log(`Model: ${profile.env.GEMINI_MODEL || profile.env.OPENAI_MODEL || getGoalDefaultOpenAIModel(goal)}`)
188+
console.log(`Model: ${profile.env.GEMINI_MODEL || profile.env.MISTRAL_MODEL || profile.env.OPENAI_MODEL || getGoalDefaultOpenAIModel(goal)}`)
173189
console.log(`Path: ${outputPath}`)
174190
console.log('Next: bun run dev:profile')
175191
}

scripts/provider-launch.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ function parseLaunchOptions(argv: string[]): LaunchOptions {
5050
continue
5151
}
5252

53-
if ((lower === 'auto' || lower === 'openai' || lower === 'ollama' || lower === 'codex' || lower === 'gemini' || lower === 'atomic-chat') && requestedProfile === 'auto') {
53+
if ((lower === 'auto' || lower === 'openai' || lower === 'ollama' || lower === 'codex' || lower === 'gemini' || lower ==='mistral' || lower === 'atomic-chat') && requestedProfile === 'auto') {
5454
requestedProfile = lower as ProviderProfile | 'auto'
5555
continue
5656
}
@@ -124,6 +124,8 @@ function printSummary(profile: ProviderProfile): void {
124124
console.log(`Launching profile: ${profile}`)
125125
if (profile === 'gemini') {
126126
console.log('Using configured Gemini provider settings.')
127+
} else if (profile === 'mistral') {
128+
console.log('Using configured Mistral provider settings.')
127129
} else if (profile === 'codex') {
128130
console.log('Using configured Codex/OpenAI-compatible provider settings.')
129131
} else if (profile === 'atomic-chat') {
@@ -139,7 +141,7 @@ async function main(): Promise<void> {
139141
const options = parseLaunchOptions(process.argv.slice(2))
140142
const requestedProfile = options.requestedProfile
141143
if (!requestedProfile) {
142-
console.error('Usage: bun run scripts/provider-launch.ts [openai|ollama|codex|gemini|atomic-chat|auto] [--fast] [--goal <latency|balanced|coding>] [-- <cli args>]')
144+
console.error('Usage: bun run scripts/provider-launch.ts [openai|ollama|codex|gemini|mistral|atomic-chat|mistral|auto] [--fast] [--goal <latency|balanced|coding>] [-- <cli args>]')
143145
process.exit(1)
144146
}
145147

@@ -205,6 +207,11 @@ async function main(): Promise<void> {
205207
process.exit(1)
206208
}
207209

210+
if (profile === 'mistral' && !env.MISTRAL_API_KEY) {
211+
console.error('MISTRAL_API_KEY is required for mistral profile. Run: bun run profile:init -- --provider mistral --api-key <key>')
212+
process.exit(1)
213+
}
214+
208215
if (profile === 'openai' && (!env.OPENAI_API_KEY || env.OPENAI_API_KEY === 'SUA_CHAVE')) {
209216
console.error('OPENAI_API_KEY is required for openai profile and cannot be SUA_CHAVE. Run: bun run profile:init -- --provider openai --api-key <key>')
210217
process.exit(1)

scripts/system-check.ts

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,12 +118,16 @@ function isLocalBaseUrl(baseUrl: string): boolean {
118118
}
119119

120120
const GEMINI_DEFAULT_BASE_URL = 'https://generativelanguage.googleapis.com/v1beta/openai'
121+
const MISTRAL_DEFAULT_BASE_URL = 'https://api.mistral.ai/v1'
121122
const GITHUB_COPILOT_BASE = 'https://api.githubcopilot.com'
122123

123124
function currentBaseUrl(): string {
124125
if (isTruthy(process.env.CLAUDE_CODE_USE_GEMINI)) {
125126
return process.env.GEMINI_BASE_URL ?? GEMINI_DEFAULT_BASE_URL
126127
}
128+
if (isTruthy(process.env.CLAUDE_CODE_USE_MISTRAL)) {
129+
return process.env.MISTRAL_BASE_URL ?? process.env.MISTRAL_DEFAULT_BASE_URL
130+
}
127131
if (isTruthy(process.env.CLAUDE_CODE_USE_GITHUB)) {
128132
return process.env.OPENAI_BASE_URL ?? GITHUB_COPILOT_BASE
129133
}
@@ -155,6 +159,31 @@ function checkGeminiEnv(): CheckResult[] {
155159
return results
156160
}
157161

162+
function checkMistralEnv(): CheckResult[] {
163+
const results: CheckResult[] = []
164+
const model = process.env.MISTRAL_MODEL
165+
const key = process.env.MISTRAL_API_KEY
166+
const baseUrl = process.env.MISTRAL_BASE_URL ?? MISTRAL_DEFAULT_BASE_URL
167+
168+
results.push(pass('Provider mode', 'Mistral provider enabled.'))
169+
170+
if (!model) {
171+
results.push(pass('MISTRAL_MODEL', 'Not set. Default will be used at runtime.'))
172+
} else {
173+
results.push(pass('MISTRAL_MODEL', model))
174+
}
175+
176+
results.push(pass('MISTRAL_BASE_URL', baseUrl))
177+
178+
if (!key) {
179+
results.push(fail('MISTRAL_API_KEY', 'Missing. Set MISTRAL_API_KEY.'))
180+
} else {
181+
results.push(pass('MISTRAL_API_KEY', 'Configured.'))
182+
}
183+
184+
return results
185+
}
186+
158187
function checkGithubEnv(): CheckResult[] {
159188
const results: CheckResult[] = []
160189
const baseUrl = process.env.OPENAI_BASE_URL ?? GITHUB_COPILOT_BASE
@@ -186,12 +215,17 @@ function checkOpenAIEnv(): CheckResult[] {
186215
const results: CheckResult[] = []
187216
const useGemini = isTruthy(process.env.CLAUDE_CODE_USE_GEMINI)
188217
const useGithub = isTruthy(process.env.CLAUDE_CODE_USE_GITHUB)
218+
const useMistral = isTruthy(process.env.CLAUDE_CODE_USE_MISTRAL)
189219
const useOpenAI = isTruthy(process.env.CLAUDE_CODE_USE_OPENAI)
190220

191221
if (useGemini) {
192222
return checkGeminiEnv()
193223
}
194224

225+
if (useMistral) {
226+
return checkMistralEnv()
227+
}
228+
195229
if (useGithub && !useOpenAI) {
196230
return checkGithubEnv()
197231
}
@@ -268,8 +302,9 @@ async function checkBaseUrlReachability(): Promise<CheckResult> {
268302
const useGemini = isTruthy(process.env.CLAUDE_CODE_USE_GEMINI)
269303
const useOpenAI = isTruthy(process.env.CLAUDE_CODE_USE_OPENAI)
270304
const useGithub = isTruthy(process.env.CLAUDE_CODE_USE_GITHUB)
305+
const useMistral = isTruthy(process.env.CLAUDE_CODE_USE_MISTRAL)
271306

272-
if (!useGemini && !useOpenAI && !useGithub) {
307+
if (!useGemini && !useOpenAI && !useGithub && !useMistral) {
273308
return pass('Provider reachability', 'Skipped (OpenAI-compatible mode disabled).')
274309
}
275310

@@ -326,6 +361,8 @@ async function checkBaseUrlReachability(): Promise<CheckResult> {
326361
})
327362
} else if (useGemini && (process.env.GEMINI_API_KEY ?? process.env.GOOGLE_API_KEY)) {
328363
headers.Authorization = `Bearer ${process.env.GEMINI_API_KEY ?? process.env.GOOGLE_API_KEY}`
364+
} else if (useMistral && process.env.MISTRAL_API_KEY) {
365+
headers.Authorization = `Bearer ${process.env.MISTRAL_API_KEY}`
329366
} else if (process.env.OPENAI_API_KEY) {
330367
headers.Authorization = `Bearer ${process.env.OPENAI_API_KEY}`
331368
}
@@ -373,7 +410,8 @@ function checkOllamaProcessorMode(): CheckResult {
373410
if (
374411
!isTruthy(process.env.CLAUDE_CODE_USE_OPENAI) ||
375412
isTruthy(process.env.CLAUDE_CODE_USE_GEMINI) ||
376-
isTruthy(process.env.CLAUDE_CODE_USE_GITHUB)
413+
isTruthy(process.env.CLAUDE_CODE_USE_GITHUB) ||
414+
isTruthy(process.env.CLAUDE_CODE_USE_MISTRAL)
377415
) {
378416
return pass('Ollama processor mode', 'Skipped (OpenAI-compatible mode disabled).')
379417
}
@@ -425,6 +463,14 @@ function serializeSafeEnvSummary(): Record<string, string | boolean> {
425463
GEMINI_API_KEY_SET: Boolean(process.env.GEMINI_API_KEY ?? process.env.GOOGLE_API_KEY),
426464
}
427465
}
466+
if (isTruthy(process.env.CLAUDE_CODE_USE_MISTRAL)) {
467+
return {
468+
CLAUDE_CODE_USE_MISTRAL: true,
469+
MISTRAL_MODEL: process.env.MISTRAL_MODEL ?? '(unset, default: devstral-latest)',
470+
MISTRAL_BASE_URL: process.env.MISTRAL_BASE_URL ?? 'https://api.mistral.ai/v1',
471+
MISTRAL_API_KEY_SET: Boolean(process.env.MISTRAL_API_KEY),
472+
}
473+
}
428474
if (
429475
isTruthy(process.env.CLAUDE_CODE_USE_GITHUB) &&
430476
!isTruthy(process.env.CLAUDE_CODE_USE_OPENAI)

src/commands/model/model.test.tsx

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,44 @@
11
import { afterEach, expect, mock, test } from 'bun:test'
22

3+
import { getAdditionalModelOptionsCacheScope } from '../../services/api/providerConfig.js'
4+
import { getAPIProvider } from '../../utils/model/providers.js'
5+
36
const originalEnv = {
47
CLAUDE_CODE_USE_OPENAI: process.env.CLAUDE_CODE_USE_OPENAI,
8+
CLAUDE_CODE_USE_GEMINI: process.env.CLAUDE_CODE_USE_GEMINI,
9+
CLAUDE_CODE_USE_GITHUB: process.env.CLAUDE_CODE_USE_GITHUB,
10+
CLAUDE_CODE_USE_MISTRAL: process.env.CLAUDE_CODE_USE_MISTRAL,
11+
CLAUDE_CODE_USE_BEDROCK: process.env.CLAUDE_CODE_USE_BEDROCK,
12+
CLAUDE_CODE_USE_VERTEX: process.env.CLAUDE_CODE_USE_VERTEX,
13+
CLAUDE_CODE_USE_FOUNDRY: process.env.CLAUDE_CODE_USE_FOUNDRY,
514
OPENAI_BASE_URL: process.env.OPENAI_BASE_URL,
15+
OPENAI_API_BASE: process.env.OPENAI_API_BASE,
616
OPENAI_MODEL: process.env.OPENAI_MODEL,
717
}
818

919
afterEach(() => {
1020
mock.restore()
1121
process.env.CLAUDE_CODE_USE_OPENAI = originalEnv.CLAUDE_CODE_USE_OPENAI
22+
process.env.CLAUDE_CODE_USE_GEMINI = originalEnv.CLAUDE_CODE_USE_GEMINI
23+
process.env.CLAUDE_CODE_USE_GITHUB = originalEnv.CLAUDE_CODE_USE_GITHUB
24+
process.env.CLAUDE_CODE_USE_MISTRAL = originalEnv.CLAUDE_CODE_USE_MISTRAL
25+
process.env.CLAUDE_CODE_USE_BEDROCK = originalEnv.CLAUDE_CODE_USE_BEDROCK
26+
process.env.CLAUDE_CODE_USE_VERTEX = originalEnv.CLAUDE_CODE_USE_VERTEX
27+
process.env.CLAUDE_CODE_USE_FOUNDRY = originalEnv.CLAUDE_CODE_USE_FOUNDRY
1228
process.env.OPENAI_BASE_URL = originalEnv.OPENAI_BASE_URL
29+
process.env.OPENAI_API_BASE = originalEnv.OPENAI_API_BASE
1330
process.env.OPENAI_MODEL = originalEnv.OPENAI_MODEL
1431
})
1532

1633
test('opens the model picker without awaiting local model discovery refresh', async () => {
1734
process.env.CLAUDE_CODE_USE_OPENAI = '1'
35+
delete process.env.CLAUDE_CODE_USE_GEMINI
36+
delete process.env.CLAUDE_CODE_USE_GITHUB
37+
delete process.env.CLAUDE_CODE_USE_MISTRAL
38+
delete process.env.CLAUDE_CODE_USE_BEDROCK
39+
delete process.env.CLAUDE_CODE_USE_VERTEX
40+
delete process.env.CLAUDE_CODE_USE_FOUNDRY
41+
delete process.env.OPENAI_API_BASE
1842
process.env.OPENAI_BASE_URL = 'http://127.0.0.1:8080/v1'
1943
process.env.OPENAI_MODEL = 'qwen2.5-coder-7b-instruct'
2044

@@ -30,7 +54,9 @@ test('opens the model picker without awaiting local model discovery refresh', as
3054
discoverOpenAICompatibleModelOptions,
3155
}))
3256

33-
const { call } = await import(`./model.js?ts=${Date.now()}-${Math.random()}`)
57+
expect(getAdditionalModelOptionsCacheScope()).toBe('openai:http://127.0.0.1:8080/v1')
58+
59+
const { call } = await import('./model.js')
3460
const result = await Promise.race([
3561
call(() => {}, {} as never, ''),
3662
new Promise(resolve => setTimeout(() => resolve('timeout'), 50)),

src/commands/model/model.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,7 @@ function haveSameModelOptions(left: ModelOption[], right: ModelOption[]): boolea
284284
});
285285
}
286286
async function refreshOpenAIModelOptionsCache(): Promise<void> {
287-
if (getAPIProvider() !== 'openai') {
287+
if (!getAdditionalModelOptionsCacheScope()?.startsWith('openai:')) {
288288
return;
289289
}
290290
try {

0 commit comments

Comments
 (0)