Skip to content

Commit 4ffb09c

Browse files
committed
feat: add custom model management system with add/remove/reset functionality
1 parent 2463120 commit 4ffb09c

21 files changed

Lines changed: 1106 additions & 497 deletions

File tree

src/main/ipc/channels.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ export const IpcChannels = {
2929
PROVIDERS_IMPORT: 'providers:import',
3030
PROVIDERS_SYNC_MODELS: 'providers:syncModels',
3131
PROVIDERS_UPDATE_MODELS: 'providers:updateModels',
32+
PROVIDERS_GET_EFFECTIVE_MODELS: 'providers:getEffectiveModels',
33+
PROVIDERS_ADD_CUSTOM_MODEL: 'providers:addCustomModel',
34+
PROVIDERS_REMOVE_MODEL: 'providers:removeModel',
35+
PROVIDERS_RESET_MODELS: 'providers:resetModels',
3236

3337
ACCOUNTS_GET_ALL: 'accounts:getAll',
3438
ACCOUNTS_GET_BY_ID: 'accounts:getById',

src/main/ipc/handlers.ts

Lines changed: 63 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -355,23 +355,19 @@ export async function registerIpcHandlers(mainWindow: BrowserWindow | null): Pro
355355
}
356356
}
357357

358-
// Try to get an active account for authentication (include credentials)
359358
const accounts = AccountManager.getByProviderId(providerId, true)
360359
const activeAccount = accounts.find(a => a.status === 'active')
361360

362-
// Build request headers with optional authentication
363361
const requestHeaders: Record<string, string> = {
364362
'Content-Type': 'application/json',
365363
Accept: 'application/json',
366364
...modelsApiHeaders,
367365
}
368366

369-
// Add authorization header if account exists
370367
if (activeAccount?.credentials?.token) {
371368
requestHeaders['Authorization'] = `Bearer ${activeAccount.credentials.token}`
372369
}
373370

374-
// Add cookie header if account has cookies (required for qwen-ai)
375371
if (activeAccount?.credentials?.cookies) {
376372
requestHeaders['Cookie'] = activeAccount.credentials.cookies
377373
}
@@ -441,6 +437,63 @@ export async function registerIpcHandlers(mainWindow: BrowserWindow | null): Pro
441437
}
442438
})
443439

440+
ipcMain.handle(IpcChannels.PROVIDERS_GET_EFFECTIVE_MODELS, async (_, providerId: string) => {
441+
try {
442+
return storeManager.getEffectiveModels(providerId)
443+
} catch (error) {
444+
console.error('[IPC] Failed to get effective models:', error)
445+
return []
446+
}
447+
})
448+
449+
ipcMain.handle(IpcChannels.PROVIDERS_ADD_CUSTOM_MODEL, async (_, providerId: string, model: { displayName: string; actualModelId: string }) => {
450+
try {
451+
return {
452+
success: true,
453+
models: storeManager.addCustomModel(providerId, model),
454+
}
455+
} catch (error) {
456+
console.error('[IPC] Failed to add custom model:', error)
457+
return {
458+
success: false,
459+
error: error instanceof Error ? error.message : 'Failed to add custom model',
460+
models: [],
461+
}
462+
}
463+
})
464+
465+
ipcMain.handle(IpcChannels.PROVIDERS_REMOVE_MODEL, async (_, providerId: string, modelName: string) => {
466+
try {
467+
return {
468+
success: true,
469+
models: storeManager.removeModel(providerId, modelName),
470+
}
471+
} catch (error) {
472+
console.error('[IPC] Failed to remove model:', error)
473+
return {
474+
success: false,
475+
error: error instanceof Error ? error.message : 'Failed to remove model',
476+
models: [],
477+
}
478+
}
479+
})
480+
481+
ipcMain.handle(IpcChannels.PROVIDERS_RESET_MODELS, async (_, providerId: string) => {
482+
try {
483+
return {
484+
success: true,
485+
models: storeManager.resetModels(providerId),
486+
}
487+
} catch (error) {
488+
console.error('[IPC] Failed to reset models:', error)
489+
return {
490+
success: false,
491+
error: error instanceof Error ? error.message : 'Failed to reset models',
492+
models: [],
493+
}
494+
}
495+
})
496+
444497
ipcMain.handle(IpcChannels.ACCOUNTS_GET_ALL, async (_, includeCredentials?: boolean): Promise<Account[]> => {
445498
return AccountManager.getAll(includeCredentials)
446499
})
@@ -558,7 +611,7 @@ export async function registerIpcHandlers(mainWindow: BrowserWindow | null): Pro
558611
return { success: false, error: 'Provider not found' }
559612
}
560613

561-
// Support qwen-ai, minimax, zai, perplexity, deepseek, and glm providers
614+
// Support qwen-ai, minimax, zai, perplexity, deepseek, glm, and mimo providers
562615
if (provider.id === 'qwen-ai') {
563616
const { QwenAiAdapter } = await import('../proxy/adapters/qwen-ai')
564617
const adapter = new QwenAiAdapter(provider, account)
@@ -589,6 +642,11 @@ export async function registerIpcHandlers(mainWindow: BrowserWindow | null): Pro
589642
const adapter = new GLMAdapter(provider, account)
590643
const success = await adapter.deleteAllChats()
591644
return { success }
645+
} else if (provider.id === 'mimo') {
646+
const { MimoAdapter } = await import('../proxy/adapters/mimo')
647+
const adapter = new MimoAdapter(provider, account)
648+
const success = await adapter.deleteAllChats()
649+
return { success }
592650
} else {
593651
return { success: false, error: 'This feature is not available for this provider' }
594652
}

src/main/providers/builtin/minimax.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,11 @@ export const minimaxConfig: BuiltinProviderConfig = {
2727
description: 'MiniMax Agent - AI assistant with MCP multi-agent collaboration',
2828
supportedModels: [
2929
'MiniMax-M2.5',
30+
'MiniMax-M2.7',
3031
],
3132
modelMappings: {
3233
'MiniMax-M2.5': 'MiniMax-M2.5',
34+
'MiniMax-M2.7': 'MiniMax-M2.7',
3335
},
3436
credentialFields: [
3537
{

src/main/proxy/adapters/mimo.ts

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,123 @@ export class MimoAdapter {
357357

358358
return { response, conversationId }
359359
}
360+
361+
private async getConversationList(pageNum: number = 1, pageSize: number = 100): Promise<{
362+
conversationIds: string[]
363+
hasMore: boolean
364+
}> {
365+
const { serviceToken, userId, phToken } = this.getCredentials()
366+
367+
if (!serviceToken || !userId || !phToken) {
368+
throw new Error('Mimo credentials not configured')
369+
}
370+
371+
const url = `${MIMO_API_BASE}/open-apis/chat/conversation/list?xiaomichatbot_ph=${encodeURIComponent(phToken)}`
372+
373+
const response = await axios.post(
374+
url,
375+
{
376+
pageInfo: {
377+
pageNum,
378+
pageSize,
379+
},
380+
},
381+
{
382+
headers: {
383+
'Content-Type': 'application/json',
384+
Cookie: `serviceToken=${serviceToken}; userId=${userId}; xiaomichatbot_ph=${phToken}`,
385+
Origin: MIMO_API_BASE,
386+
Referer: `${MIMO_API_BASE}/`,
387+
},
388+
timeout: 30000,
389+
validateStatus: () => true,
390+
}
391+
)
392+
393+
console.log('[Mimo] Get conversation list page', pageNum, 'response:', JSON.stringify(response.data, null, 2))
394+
395+
const { code, data } = response.data || {}
396+
if (response.status !== 200 || code !== 0) {
397+
console.error('[Mimo] Failed to get conversation list')
398+
return { conversationIds: [], hasMore: false }
399+
}
400+
401+
const conversationList = data?.dataList || []
402+
const conversationIds = conversationList.map((c: any) => c.conversationId).filter(Boolean)
403+
const hasMore = conversationList.length >= pageSize
404+
405+
console.log('[Mimo] Found', conversationIds.length, 'conversations, hasMore:', hasMore)
406+
return { conversationIds, hasMore }
407+
}
408+
409+
private async deleteConversations(conversationIds: string[]): Promise<boolean> {
410+
if (conversationIds.length === 0) {
411+
return true
412+
}
413+
414+
const { serviceToken, userId, phToken } = this.getCredentials()
415+
416+
if (!serviceToken || !userId || !phToken) {
417+
throw new Error('Mimo credentials not configured')
418+
}
419+
420+
const url = `${MIMO_API_BASE}/open-apis/chat/conversation/delete?xiaomichatbot_ph=${encodeURIComponent(phToken)}`
421+
422+
const response = await axios.post(
423+
url,
424+
conversationIds,
425+
{
426+
headers: {
427+
'Content-Type': 'application/json',
428+
Cookie: `serviceToken=${serviceToken}; userId=${userId}; xiaomichatbot_ph=${phToken}`,
429+
Origin: MIMO_API_BASE,
430+
Referer: `${MIMO_API_BASE}/`,
431+
},
432+
timeout: 60000,
433+
validateStatus: () => true,
434+
}
435+
)
436+
437+
console.log('[Mimo] Delete conversations response:', JSON.stringify(response.data, null, 2))
438+
439+
const { code } = response.data || {}
440+
return response.status === 200 && code === 0
441+
}
442+
443+
async deleteAllChats(): Promise<boolean> {
444+
try {
445+
const allConversationIds: string[] = []
446+
let pageNum = 1
447+
let hasMore = true
448+
449+
while (hasMore) {
450+
const { conversationIds, hasMore: more } = await this.getConversationList(pageNum, 100)
451+
allConversationIds.push(...conversationIds)
452+
hasMore = more
453+
pageNum++
454+
455+
if (conversationIds.length === 0) {
456+
break
457+
}
458+
}
459+
460+
if (allConversationIds.length === 0) {
461+
console.log('[Mimo] No conversations to delete')
462+
return true
463+
}
464+
465+
console.log('[Mimo] Found', allConversationIds.length, 'conversations to delete')
466+
467+
const success = await this.deleteConversations(allConversationIds)
468+
if (success) {
469+
console.log('[Mimo] All chats deleted')
470+
}
471+
return success
472+
} catch (error) {
473+
console.error('[Mimo] Failed to delete all chats:', error)
474+
return false
475+
}
476+
}
360477
}
361478

362479
export class MimoStreamHandler {

src/main/proxy/loadbalancer.ts

Lines changed: 24 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -129,13 +129,14 @@ export class LoadBalancer {
129129
* Check if provider supports model
130130
*/
131131
private providerSupportsModel(provider: Provider, model: string): boolean {
132-
if (!provider.supportedModels || provider.supportedModels.length === 0) {
132+
const effectiveModels = storeManager.getEffectiveModels(provider.id)
133+
if (effectiveModels.length === 0) {
133134
return true
134135
}
135136

136137
const normalizedModel = model.toLowerCase()
137-
const supported = provider.supportedModels.some(m => {
138-
const normalizedSupported = m.toLowerCase()
138+
const supported = effectiveModels.some(m => {
139+
const normalizedSupported = m.displayName.toLowerCase()
139140
if (normalizedSupported.endsWith('*')) {
140141
return normalizedModel.startsWith(normalizedSupported.slice(0, -1))
141142
}
@@ -146,17 +147,9 @@ export class LoadBalancer {
146147
return true
147148
}
148149

149-
// Check if model is in provider's model mappings
150-
if (provider.modelMappings && provider.modelMappings[model]) {
151-
console.log(`[LoadBalancer] Model "${model}" found in provider model mappings`)
152-
return true
153-
}
154-
155-
// Check global model mappings
156150
const config = storeManager.getConfig()
157151
const globalMapping = config.modelMappings[model]
158152
if (globalMapping) {
159-
// If preferredProviderId is set, only match that provider
160153
if (globalMapping.preferredProviderId) {
161154
if (globalMapping.preferredProviderId === provider.id) {
162155
console.log(`[LoadBalancer] Model "${model}" matched preferred provider ${provider.name}`)
@@ -165,11 +158,10 @@ export class LoadBalancer {
165158
return false
166159
}
167160

168-
// If no preferredProviderId, check if provider supports the actualModel
169161
const actualModel = globalMapping.actualModel
170162
const normalizedActualModel = actualModel.toLowerCase()
171-
const actualSupported = provider.supportedModels.some(m => {
172-
const normalizedSupported = m.toLowerCase()
163+
const actualSupported = effectiveModels.some(m => {
164+
const normalizedSupported = m.displayName.toLowerCase()
173165
if (normalizedSupported.endsWith('*')) {
174166
return normalizedActualModel.startsWith(normalizedSupported.slice(0, -1))
175167
}
@@ -180,15 +172,9 @@ export class LoadBalancer {
180172
console.log(`[LoadBalancer] Model "${model}" (actualModel: "${actualModel}") supported by ${provider.name}`)
181173
return true
182174
}
183-
184-
// Also check provider's model mappings for actualModel
185-
if (provider.modelMappings && provider.modelMappings[actualModel]) {
186-
console.log(`[LoadBalancer] Model "${model}" (actualModel: "${actualModel}") found in provider model mappings`)
187-
return true
188-
}
189175
}
190176

191-
console.log(`[LoadBalancer] Provider ${provider.name} does not support model ${model}, supported models:`, provider.supportedModels)
177+
console.log(`[LoadBalancer] Provider ${provider.name} does not support model ${model}`)
192178
return false
193179
}
194180

@@ -212,28 +198,30 @@ export class LoadBalancer {
212198
*/
213199
private mapModel(model: string, provider: Provider): string {
214200
console.log(`[LoadBalancer] mapModel called with model="${model}", provider="${provider.name}"`)
215-
console.log(`[LoadBalancer] provider.modelMappings:`, provider.modelMappings)
216201

217-
// First check provider-level model mapping
218-
if (provider.modelMappings && provider.modelMappings[model]) {
219-
const mapped = provider.modelMappings[model]
220-
console.log(`[LoadBalancer] Model mapped from "${model}" to "${mapped}" via provider mapping`)
221-
return mapped
202+
const effectiveModels = storeManager.getEffectiveModels(provider.id)
203+
const effectiveModel = effectiveModels.find(m =>
204+
m.displayName.toLowerCase() === model.toLowerCase()
205+
)
206+
207+
if (effectiveModel) {
208+
console.log(`[LoadBalancer] Model mapped from "${model}" to "${effectiveModel.actualModelId}" via effective models`)
209+
return effectiveModel.actualModelId
222210
}
223211

224-
// Then check global config model mapping
225212
const config = storeManager.getConfig()
226213
const mapping = config.modelMappings[model]
227214

228215
if (mapping && (!mapping.preferredProviderId || mapping.preferredProviderId === provider.id)) {
229216
const actualModel = mapping.actualModel
230217
console.log(`[LoadBalancer] Model mapped from "${model}" to "${actualModel}" via global mapping`)
231218

232-
// After global mapping, check if provider has a mapping for the actual model
233-
if (provider.modelMappings && provider.modelMappings[actualModel]) {
234-
const finalModel = provider.modelMappings[actualModel]
235-
console.log(`[LoadBalancer] Model further mapped from "${actualModel}" to "${finalModel}" via provider mapping`)
236-
return finalModel
219+
const actualEffectiveModel = effectiveModels.find(m =>
220+
m.displayName.toLowerCase() === actualModel.toLowerCase()
221+
)
222+
if (actualEffectiveModel) {
223+
console.log(`[LoadBalancer] Model further mapped from "${actualModel}" to "${actualEffectiveModel.actualModelId}" via effective models`)
224+
return actualEffectiveModel.actualModelId
237225
}
238226

239227
return actualModel
@@ -340,8 +328,9 @@ export class LoadBalancer {
340328
const accounts = storeManager.getAccountsByProviderId(provider.id)
341329
.filter(account => this.isAccountAvailable(account))
342330

343-
if (accounts.length > 0 && provider.supportedModels) {
344-
provider.supportedModels.forEach(m => models.add(m))
331+
if (accounts.length > 0) {
332+
const effectiveModels = storeManager.getEffectiveModels(provider.id)
333+
effectiveModels.forEach(m => models.add(m.displayName))
345334
}
346335
}
347336

src/main/proxy/modelMapper.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -169,13 +169,14 @@ export class ModelMapper {
169169
}
170170

171171
return providers.filter(provider => {
172-
if (!provider.supportedModels || provider.supportedModels.length === 0) {
172+
const effectiveModels = storeManager.getEffectiveModels(provider.id)
173+
if (effectiveModels.length === 0) {
173174
return true
174175
}
175176

176177
const normalizedModel = model.toLowerCase()
177-
return provider.supportedModels.some(m => {
178-
const normalizedSupported = m.toLowerCase()
178+
return effectiveModels.some(m => {
179+
const normalizedSupported = m.displayName.toLowerCase()
179180
if (normalizedSupported.endsWith('*')) {
180181
return normalizedModel.startsWith(normalizedSupported.slice(0, -1))
181182
}

0 commit comments

Comments
 (0)