Skip to content

Commit 2463120

Browse files
committed
feat: add dynamic model update for providers and fix MiniMax stream handling
1 parent 953118a commit 2463120

16 files changed

Lines changed: 407 additions & 66 deletions

File tree

src/main/ipc/channels.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ export const IpcChannels = {
2727
PROVIDERS_DUPLICATE: 'providers:duplicate',
2828
PROVIDERS_EXPORT: 'providers:export',
2929
PROVIDERS_IMPORT: 'providers:import',
30+
PROVIDERS_SYNC_MODELS: 'providers:syncModels',
31+
PROVIDERS_UPDATE_MODELS: 'providers:updateModels',
3032

3133
ACCOUNTS_GET_ALL: 'accounts:getAll',
3234
ACCOUNTS_GET_BY_ID: 'accounts:getById',

src/main/ipc/handlers.ts

Lines changed: 155 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { ConfigManager } from '../store/config'
1616
import { generateManagementSecret } from '../proxy/middleware/managementAuth'
1717
import type { Provider, Account, ProxyStatus, ProviderCheckResult, OAuthResult, AuthType, CredentialField, LogLevel, LogEntry, ProviderVendor, AppConfig } from '../../shared/types'
1818
import type { SystemPrompt, SessionConfig, SessionRecord, ManagementApiConfig } from '../store/types'
19+
import type { ProviderType } from '../oauth/types'
1920

2021
let proxyServer: ProxyServer | null = null
2122
let proxyStartTime: number | null = null
@@ -291,6 +292,155 @@ export async function registerIpcHandlers(mainWindow: BrowserWindow | null): Pro
291292
return CustomProviderManager.importProvider(jsonData)
292293
})
293294

295+
ipcMain.handle(IpcChannels.PROVIDERS_SYNC_MODELS, async (_, providerId: string): Promise<{
296+
success: boolean
297+
supportedModels?: string[]
298+
modelMappings?: Record<string, string>
299+
error?: string
300+
}> => {
301+
try {
302+
const result = await ProviderChecker.fetchProviderModels(providerId)
303+
304+
const provider = ProviderManager.getById(providerId)
305+
if (provider) {
306+
ProviderManager.update(providerId, {
307+
supportedModels: result.supportedModels,
308+
modelMappings: result.modelMappings,
309+
})
310+
}
311+
312+
return {
313+
success: true,
314+
supportedModels: result.supportedModels,
315+
modelMappings: result.modelMappings,
316+
}
317+
} catch (error) {
318+
return {
319+
success: false,
320+
error: error instanceof Error ? error.message : 'Failed to sync models',
321+
}
322+
}
323+
})
324+
325+
ipcMain.handle(IpcChannels.PROVIDERS_UPDATE_MODELS, async (_, providerId: string): Promise<{
326+
success: boolean
327+
modelsCount?: number
328+
error?: string
329+
}> => {
330+
try {
331+
const provider = ProviderManager.getById(providerId)
332+
333+
if (!provider) {
334+
return {
335+
success: false,
336+
error: 'Provider not found',
337+
}
338+
}
339+
340+
let modelsApiEndpoint: string | undefined
341+
let modelsApiHeaders: Record<string, string> | undefined
342+
343+
if (provider.type === 'builtin') {
344+
const builtinConfig = getBuiltinProvider(providerId)
345+
if (builtinConfig) {
346+
modelsApiEndpoint = builtinConfig.modelsApiEndpoint
347+
modelsApiHeaders = builtinConfig.modelsApiHeaders
348+
}
349+
}
350+
351+
if (!modelsApiEndpoint) {
352+
return {
353+
success: false,
354+
error: 'This provider does not support dynamic model updates',
355+
}
356+
}
357+
358+
// Try to get an active account for authentication (include credentials)
359+
const accounts = AccountManager.getByProviderId(providerId, true)
360+
const activeAccount = accounts.find(a => a.status === 'active')
361+
362+
// Build request headers with optional authentication
363+
const requestHeaders: Record<string, string> = {
364+
'Content-Type': 'application/json',
365+
Accept: 'application/json',
366+
...modelsApiHeaders,
367+
}
368+
369+
// Add authorization header if account exists
370+
if (activeAccount?.credentials?.token) {
371+
requestHeaders['Authorization'] = `Bearer ${activeAccount.credentials.token}`
372+
}
373+
374+
// Add cookie header if account has cookies (required for qwen-ai)
375+
if (activeAccount?.credentials?.cookies) {
376+
requestHeaders['Cookie'] = activeAccount.credentials.cookies
377+
}
378+
379+
const response = await axios.get(modelsApiEndpoint, {
380+
headers: requestHeaders,
381+
timeout: 15000,
382+
validateStatus: () => true,
383+
})
384+
385+
if (response.status !== 200) {
386+
return {
387+
success: false,
388+
error: `Failed to fetch models: HTTP ${response.status}`,
389+
}
390+
}
391+
392+
const models = response.data.data || response.data
393+
394+
if (!Array.isArray(models) || models.length === 0) {
395+
return {
396+
success: false,
397+
error: 'No models found in the response',
398+
}
399+
}
400+
401+
const supportedModels: string[] = []
402+
const modelMappings: Record<string, string> = {}
403+
404+
models.forEach((model: any) => {
405+
if (typeof model === 'string') {
406+
supportedModels.push(model)
407+
modelMappings[model] = model
408+
} else if (model && typeof model === 'object') {
409+
const modelId = model.id || model.model_id || model.name
410+
const modelName = model.name || model.display_name || modelId
411+
412+
if (modelId) {
413+
supportedModels.push(modelName || modelId)
414+
modelMappings[modelName || modelId] = modelId
415+
}
416+
}
417+
})
418+
419+
if (supportedModels.length === 0) {
420+
return {
421+
success: false,
422+
error: 'Failed to parse models from the response',
423+
}
424+
}
425+
426+
ProviderManager.update(providerId, {
427+
supportedModels,
428+
modelMappings,
429+
})
430+
431+
return {
432+
success: true,
433+
modelsCount: supportedModels.length,
434+
}
435+
} catch (error) {
436+
console.error('[IPC] Failed to update models:', error)
437+
return {
438+
success: false,
439+
error: error instanceof Error ? error.message : 'Failed to update models',
440+
}
441+
}
442+
})
443+
294444
ipcMain.handle(IpcChannels.ACCOUNTS_GET_ALL, async (_, includeCredentials?: boolean): Promise<Account[]> => {
295445
return AccountManager.getAll(includeCredentials)
296446
})
@@ -455,7 +605,7 @@ export async function registerIpcHandlers(mainWindow: BrowserWindow | null): Pro
455605
console.log('Starting OAuth login:', providerId, providerType)
456606
return await oauthManager.startLogin({
457607
providerId,
458-
providerType,
608+
providerType: providerType as ProviderType,
459609
})
460610
})
461611

@@ -465,14 +615,14 @@ export async function registerIpcHandlers(mainWindow: BrowserWindow | null): Pro
465615
})
466616

467617
ipcMain.handle(IpcChannels.OAUTH_LOGIN_WITH_TOKEN, async (_, data: { providerId: string, providerType: ProviderVendor, token: string, realUserID?: string, mimoUserId?: string, mimoPhToken?: string }): Promise<OAuthResult> => {
468-
return await oauthManager.loginWithToken(data.providerId, data.providerType, data.token, data.realUserID, data.mimoUserId, data.mimoPhToken)
618+
return await oauthManager.loginWithToken(data.providerId, data.providerType as ProviderType, data.token, data.realUserID, data.mimoUserId, data.mimoPhToken)
469619
})
470620

471621
ipcMain.handle(IpcChannels.OAUTH_START_IN_APP_LOGIN, async (_, data: { providerId: string, providerType: ProviderVendor, timeout?: number }): Promise<OAuthResult> => {
472622
console.log('Starting in-app OAuth login:', data.providerId, data.providerType)
473623
const config = storeManager.getConfig()
474624
const proxyMode = (config as any).oauthProxyMode || 'system'
475-
return await oauthManager.startInAppLogin(data.providerId, data.providerType, data.timeout, proxyMode)
625+
return await oauthManager.startInAppLogin(data.providerId, data.providerType as ProviderType, data.timeout, proxyMode)
476626
})
477627

478628
ipcMain.handle(IpcChannels.OAUTH_CANCEL_IN_APP_LOGIN, async (): Promise<void> => {
@@ -485,11 +635,11 @@ export async function registerIpcHandlers(mainWindow: BrowserWindow | null): Pro
485635
})
486636

487637
ipcMain.handle(IpcChannels.OAUTH_VALIDATE_TOKEN, async (_, data: { providerId: string, providerType: ProviderVendor, credentials: Record<string, string> }) => {
488-
return await oauthManager.validateToken(data.providerId, data.providerType, data.credentials)
638+
return await oauthManager.validateToken(data.providerId, data.providerType as ProviderType, data.credentials)
489639
})
490640

491641
ipcMain.handle(IpcChannels.OAUTH_REFRESH_TOKEN, async (_, data: { providerId: string, providerType: ProviderVendor, credentials: Record<string, string> }) => {
492-
return await oauthManager.refreshToken(data.providerId, data.providerType, data.credentials)
642+
return await oauthManager.refreshToken(data.providerId, data.providerType as ProviderType, data.credentials)
493643
})
494644

495645
ipcMain.handle(IpcChannels.OAUTH_GET_STATUS, async (): Promise<string> => {

src/main/providers/builtin/qwen-ai.ts

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,25 @@ export const qwenAiConfig: BuiltinProviderConfig = {
1414
},
1515
enabled: true,
1616
description: 'Qwen AI international version (chat.qwen.ai)',
17+
modelsApiEndpoint: 'https://chat.qwen.ai/api/models',
18+
modelsApiHeaders: {
19+
Accept: 'application/json, text/plain, */*',
20+
Referer: 'https://chat.qwen.ai/',
21+
source: 'web',
22+
Version: '0.2.35',
23+
},
1724
supportedModels: [
25+
'Qwen3.6-Plus',
1826
'Qwen3.5-Plus',
27+
'Qwen3.5-Omni-Plus',
28+
'Qwen3.5-Flash',
29+
'Qwen3.5-Max-Preview',
30+
'Qwen3.6-Plus-Preview',
1931
'Qwen3.5-397B-A17B',
32+
'Qwen3.5-122B-A10B',
33+
'Qwen3.5-Omni-Flash',
34+
'Qwen3.5-27B',
35+
'Qwen3.5-35B-A3B',
2036
'Qwen3-Max',
2137
'Qwen3-235B-A22B-2507',
2238
'Qwen3-Coder',
@@ -25,14 +41,23 @@ export const qwenAiConfig: BuiltinProviderConfig = {
2541
'Qwen2.5-Max',
2642
],
2743
modelMappings: {
44+
'Qwen3.6-Plus': 'qwen3.6-plus',
2845
'Qwen3.5-Plus': 'qwen3.5-plus',
46+
'Qwen3.5-Omni-Plus': 'qwen3.5-omni-plus',
47+
'Qwen3.5-Flash': 'qwen3.5-flash',
48+
'Qwen3.5-Max-Preview': 'qwen3.5-max-2026-03-08',
49+
'Qwen3.6-Plus-Preview': 'qwen3.6-plus-preview',
2950
'Qwen3.5-397B-A17B': 'qwen3.5-397b-a17b',
30-
'Qwen3-Max': 'qwen3-max',
31-
'Qwen3-235B-A22B-2507': 'qwen3-235b-a22b-2507',
51+
'Qwen3.5-122B-A10B': 'qwen3.5-122b-a10b',
52+
'Qwen3.5-Omni-Flash': 'qwen3.5-omni-flash',
53+
'Qwen3.5-27B': 'qwen3.5-27b',
54+
'Qwen3.5-35B-A3B': 'qwen3.5-35b-a3b',
55+
'Qwen3-Max': 'qwen3-max-2026-01-23',
56+
'Qwen3-235B-A22B-2507': 'qwen-plus-2025-07-28',
3257
'Qwen3-Coder': 'qwen3-coder-plus',
33-
'Qwen3-VL-235B-A22B': 'qwen3-vl-235b-a22b',
34-
'Qwen3-Omni-Flash': 'qwen3-omni-flash',
35-
'Qwen2.5-Max': 'qwen2.5-max',
58+
'Qwen3-VL-235B-A22B': 'qwen3-vl-plus',
59+
'Qwen3-Omni-Flash': 'qwen3-omni-flash-2025-12-01',
60+
'Qwen2.5-Max': 'qwen-max-latest',
3661
},
3762
credentialFields: [
3863
{

src/main/providers/checker.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -694,6 +694,55 @@ export class ProviderChecker {
694694
const secret = '8a1317a7468aa3ad86e997d08f3f31cb'
695695
return crypto.createHash('md5').update(`${timestamp}-${nonce}-${secret}`).digest('hex')
696696
}
697+
698+
static async fetchProviderModels(
699+
providerId: string
700+
): Promise<{
701+
supportedModels: string[]
702+
modelMappings: Record<string, string>
703+
}> {
704+
const builtinConfig = getBuiltinProvider(providerId)
705+
706+
if (!builtinConfig) {
707+
throw new Error(`Provider ${providerId} not found`)
708+
}
709+
710+
if (!builtinConfig.modelsApiEndpoint) {
711+
throw new Error(`Provider ${providerId} does not support dynamic model fetching`)
712+
}
713+
714+
try {
715+
const headers: Record<string, string> = {
716+
...(builtinConfig.modelsApiHeaders || builtinConfig.headers),
717+
}
718+
719+
const response = await axios.get(builtinConfig.modelsApiEndpoint, {
720+
headers,
721+
timeout: CHECK_TIMEOUT,
722+
validateStatus: () => true,
723+
})
724+
725+
if (response.status !== 200) {
726+
throw new Error(`Failed to fetch models: HTTP ${response.status}`)
727+
}
728+
729+
const models = response.data.data || []
730+
const supportedModels: string[] = []
731+
const modelMappings: Record<string, string> = {}
732+
733+
for (const model of models) {
734+
if (model.name && model.id) {
735+
supportedModels.push(model.name)
736+
modelMappings[model.name] = model.id
737+
}
738+
}
739+
740+
return { supportedModels, modelMappings }
741+
} catch (error) {
742+
console.error(`[ProviderChecker] Failed to fetch models for ${providerId}:`, error)
743+
throw error
744+
}
745+
}
697746
}
698747

699748
export default ProviderChecker

0 commit comments

Comments
 (0)