Skip to content

Commit cf28588

Browse files
committed
fix: stabilize Z.ai and MiniMax provider inference
1 parent 3b3bc70 commit cf28588

14 files changed

Lines changed: 849 additions & 66 deletions

File tree

src/components/Settings.vue

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,10 @@ import { PREDEFINED_OPENAI_TOOLS } from '../utils/assistantTools'
216216
import { DEFAULT_ASSISTANT_PERSONA_PROMPT } from '../stores/settingsStore'
217217
import { useHotkeyRecording } from '../composables/useHotkeyRecording'
218218
import { useGoogleAuth } from '../composables/useGoogleAuth'
219+
import {
220+
getStaticModelsForProvider,
221+
type ProviderModelDefinition,
222+
} from '../services/llmProviders/providerCatalog'
219223
import CoreSettingsTab from './settings/CoreSettingsTab.vue'
220224
import AssistantSettingsTab from './settings/AssistantSettingsTab.vue'
221225
import HotkeysTab from './settings/HotkeysTab.vue'
@@ -272,6 +276,18 @@ const isBrowserContextToolActive = computed(() => {
272276
})
273277
274278
const availableModelsForSelect = computed(() => {
279+
const staticModels = getStaticModelsForProvider(
280+
currentSettings.value.aiProvider
281+
)
282+
if (staticModels.length > 0) {
283+
const staticModelIds = new Set(staticModels.map(model => model.id))
284+
return [
285+
...staticModels.map((model: ProviderModelDefinition) => ({
286+
id: model.id,
287+
})),
288+
...availableModels.value.filter(model => !staticModelIds.has(model.id)),
289+
]
290+
}
275291
return availableModels.value
276292
})
277293

src/components/wizard/OnboardingWizard.vue

Lines changed: 30 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,11 @@ import FinalSetupStep from './steps/FinalSetupStep.vue'
6161
import OpenAI from 'openai'
6262
import {
6363
MINIMAX_OPENAI_BASE_URL,
64+
ZAI_CODING_MODELS,
6465
ZAI_CODING_BASE_URL,
6566
type AIProviderKey,
6667
} from '../../services/llmProviders/providerCatalog'
68+
import { listMiniMaxModelsForConfig } from '../../services/llmProviders/minimax'
6769
6870
const step = ref(1)
6971
const settingsStore = useSettingsStore()
@@ -91,6 +93,10 @@ const formData = reactive({
9193
localSttLanguage: 'auto',
9294
})
9395
96+
function isAuthError(error: any): boolean {
97+
return error?.status === 401 || error?.status === 403
98+
}
99+
94100
const isTesting = reactive({
95101
openai: false,
96102
openrouter: false,
@@ -215,13 +221,25 @@ const fetchAvailableModels = async () => {
215221
return
216222
}
217223
224+
if (formData.aiProvider === 'minimax') {
225+
const models = await listMiniMaxModelsForConfig(
226+
formData.VITE_MINIMAX_API_KEY,
227+
baseURL
228+
)
229+
formData.availableModels = models.map(model => model.id)
230+
231+
if (formData.availableModels.length > 0) {
232+
formData.assistantModel = formData.availableModels[0]
233+
formData.summarizationModel = formData.availableModels[0]
234+
}
235+
return
236+
}
237+
218238
const tempClient = new OpenAI({
219239
apiKey:
220240
formData.aiProvider === 'zai'
221241
? formData.VITE_ZAI_API_KEY
222-
: formData.aiProvider === 'minimax'
223-
? formData.VITE_MINIMAX_API_KEY
224-
: formData.aiProvider,
242+
: formData.aiProvider,
225243
baseURL,
226244
dangerouslyAllowBrowser: true,
227245
})
@@ -234,8 +252,15 @@ const fetchAvailableModels = async () => {
234252
formData.summarizationModel = formData.availableModels[0]
235253
}
236254
} catch (error) {
255+
if (formData.aiProvider === 'zai' && !isAuthError(error)) {
256+
formData.availableModels = ZAI_CODING_MODELS.map(model => model.id)
257+
formData.assistantModel = formData.availableModels[0] || 'glm-5.1'
258+
formData.summarizationModel = formData.availableModels[0] || 'glm-5.1'
259+
return
260+
}
237261
console.error('Failed to fetch models:', error)
238262
formData.availableModels = []
263+
throw error
239264
}
240265
}
241266
@@ -320,17 +345,8 @@ const testZAIKey = async () => {
320345
testResult.zai.success = false
321346
322347
try {
323-
const tempClient = new OpenAI({
324-
apiKey: formData.VITE_ZAI_API_KEY,
325-
baseURL: formData.zaiBaseUrl,
326-
dangerouslyAllowBrowser: true,
327-
timeout: 10 * 1000,
328-
maxRetries: 1,
329-
})
330-
331-
await tempClient.models.list()
332-
testResult.zai.success = true
333348
await fetchAvailableModels()
349+
testResult.zai.success = true
334350
} catch (e: any) {
335351
testResult.zai.error =
336352
'API key or Coding Plan endpoint is invalid or has no permissions.'
@@ -361,17 +377,8 @@ const testMiniMaxKey = async () => {
361377
testResult.minimax.success = false
362378
363379
try {
364-
const tempClient = new OpenAI({
365-
apiKey: formData.VITE_MINIMAX_API_KEY,
366-
baseURL: formData.minimaxBaseUrl,
367-
dangerouslyAllowBrowser: true,
368-
timeout: 10 * 1000,
369-
maxRetries: 1,
370-
})
371-
372-
await tempClient.models.list()
373-
testResult.minimax.success = true
374380
await fetchAvailableModels()
381+
testResult.minimax.success = true
375382
} catch (e: any) {
376383
testResult.minimax.error =
377384
'API key or OpenAI-compatible endpoint is invalid or has no permissions.'

src/modules/conversation/__tests__/turnManager.test.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ describe('createTurnManager', () => {
155155

156156
ctx.state.setAudioPlayer({ paused: true })
157157
ctx.state.setAudioQueueLength(0)
158+
ctx.state.setAudioState('IDLE')
158159
vi.advanceTimersByTime(250)
159160

160161
expect(ctx.deps.getAudioState()).toBe('IDLE')
@@ -198,5 +199,28 @@ describe('createTurnManager', () => {
198199
expect(ctx.triggerSummarization).not.toHaveBeenCalled()
199200
manager.dispose()
200201
})
201-
})
202202

203+
it('does not force IDLE while audio playback is being prepared', () => {
204+
const ctx = buildDependencies()
205+
ctx.state.setAudioPlayer({ paused: true })
206+
ctx.state.setAudioQueueLength(0)
207+
ctx.state.setAudioState('SPEAKING')
208+
const manager = createTurnManager(ctx.deps)
209+
210+
manager.finalizeAfterStream({
211+
streamEndedNormally: true,
212+
isContinuationAfterTool: false,
213+
})
214+
215+
vi.advanceTimersByTime(250)
216+
217+
expect(ctx.deps.getAudioState()).toBe('SPEAKING')
218+
expect(ctx.triggerSummarization).not.toHaveBeenCalled()
219+
220+
ctx.state.setAudioState('IDLE')
221+
vi.advanceTimersByTime(250)
222+
223+
expect(ctx.triggerSummarization).toHaveBeenCalled()
224+
manager.dispose()
225+
})
226+
})

src/modules/conversation/turnManager.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,11 @@ export function createTurnManager(
6464
if (!streamEndedNormally) return
6565

6666
const finalizeInterval = setInterval(() => {
67+
const audioState = dependencies.getAudioState()
68+
if (audioState === 'SPEAKING') {
69+
return
70+
}
71+
6772
const audioPlayer = dependencies.getAudioPlayer()
6873
if (audioPlayer && !audioPlayer.paused) {
6974
return
@@ -73,8 +78,7 @@ export function createTurnManager(
7378
return
7479
}
7580

76-
const audioState = dependencies.getAudioState()
77-
if (audioState === 'SPEAKING' || audioState === 'WAITING_FOR_RESPONSE') {
81+
if (audioState === 'WAITING_FOR_RESPONSE') {
7882
dependencies.setAudioState(
7983
dependencies.isRecordingRequested() ? 'LISTENING' : 'IDLE'
8084
)
@@ -98,4 +102,3 @@ export function createTurnManager(
98102
dispose,
99103
}
100104
}
101-

src/services/apiService.ts

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,14 @@ import {
2828
createMiniMaxResponse,
2929
listMiniMaxModels,
3030
} from './llmProviders/minimax'
31-
import { isChatCompletionsProvider } from './llmProviders/providerCatalog'
31+
import {
32+
createMiniMaxChatCompletionViaMain,
33+
stripReasoningFromMiniMaxContent,
34+
} from './llmProviders/openAICompatible'
35+
import {
36+
getSafeProviderModel,
37+
isChatCompletionsProvider,
38+
} from './llmProviders/providerCatalog'
3239
import type { AppChatMessageContentPart } from '../types/chat'
3340
import type { RagSearchResult } from '../types/rag'
3441

@@ -715,16 +722,21 @@ export const createSummarizationResponse = async (
715722
.join('\n\n')
716723

717724
if (isChatCompletionsProvider(settings.aiProvider)) {
718-
const response = await client.chat.completions.create({
719-
model: summarizationModel,
725+
const params = {
726+
model: getSafeProviderModel(settings.aiProvider, summarizationModel),
720727
messages: [
721728
{ role: 'system', content: systemPrompt },
722729
{ role: 'user', content: combinedText },
723730
],
724731
stream: false,
725-
} as any)
726-
727-
return response.choices[0]?.message?.content?.trim() || null
732+
} as any
733+
const response =
734+
settings.aiProvider === 'minimax'
735+
? await createMiniMaxChatCompletionViaMain(params)
736+
: await client.chat.completions.create(params)
737+
738+
const content = response.choices[0]?.message?.content?.trim()
739+
return content ? stripReasoningFromMiniMaxContent(content) : null
728740
} else {
729741
const response = await client.responses.create({
730742
model: summarizationModel,
@@ -767,18 +779,23 @@ export const createContextAnalysisResponse = async (
767779
.join('\n\n')
768780

769781
if (isChatCompletionsProvider(settings.aiProvider)) {
770-
const response = await client.chat.completions.create({
771-
model: analysisModel,
782+
const params = {
783+
model: getSafeProviderModel(settings.aiProvider, analysisModel),
772784
messages: [
773785
{ role: 'system', content: analysisSystemPrompt },
774786
{ role: 'user', content: combinedText },
775787
],
776788
stream: false,
777-
} as any)
778-
779-
return (
780-
response.choices[0]?.message?.content?.trim().replace(/"/g, '') || null
781-
)
789+
} as any
790+
const response =
791+
settings.aiProvider === 'minimax'
792+
? await createMiniMaxChatCompletionViaMain(params)
793+
: await client.chat.completions.create(params)
794+
795+
const content = response.choices[0]?.message?.content?.trim()
796+
return content
797+
? stripReasoningFromMiniMaxContent(content).replace(/"/g, '')
798+
: null
782799
} else {
783800
const response = await client.responses.create({
784801
model: analysisModel,
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { afterEach, describe, expect, it, vi } from 'vitest'
2+
import { listMiniMaxModelsForConfig } from '../minimax'
3+
4+
describe('MiniMax model listing', () => {
5+
afterEach(() => {
6+
vi.restoreAllMocks()
7+
delete (globalThis as any).window
8+
})
9+
10+
it('uses the Electron HTTP bridge without OpenAI SDK stainless headers', async () => {
11+
const request = vi.fn().mockResolvedValue({
12+
success: true,
13+
status: 200,
14+
data: {
15+
data: [{ id: 'MiniMax-M2.7' }, { id: 'unrelated-model' }],
16+
},
17+
})
18+
;(globalThis as any).window = {
19+
httpAPI: {
20+
request,
21+
},
22+
}
23+
24+
const models = await listMiniMaxModelsForConfig(
25+
'sk-test',
26+
'https://api.minimax.io/v1/'
27+
)
28+
29+
expect(models.map(model => model.id)).toEqual(['MiniMax-M2.7'])
30+
expect(request).toHaveBeenCalledWith(
31+
expect.objectContaining({
32+
url: 'https://api.minimax.io/v1/models',
33+
method: 'GET',
34+
headers: {
35+
Authorization: 'Bearer sk-test',
36+
},
37+
})
38+
)
39+
expect(request.mock.calls[0][0].headers['x-stainless-os']).toBeUndefined()
40+
})
41+
})

0 commit comments

Comments
 (0)