Skip to content

Commit 4642abb

Browse files
authored
fix(provider): sync db-backed model lists (#1432)
* fix(provider): sync db-backed model lists * refactor(provider): remove provider supplements and simplify model loading
1 parent 7f1e1b1 commit 4642abb

20 files changed

Lines changed: 888 additions & 32 deletions

File tree

src/main/presenter/configPresenter/providerDbLoader.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,7 @@ export class ProviderDbLoader {
281281
this.writeMeta(meta)
282282
this.cache = sanitized
283283
try {
284-
const providersCount = Object.keys(sanitized.providers || {}).length
284+
const providersCount = Object.keys(this.cache.providers || {}).length
285285
eventBus.send(PROVIDER_DB_EVENTS.UPDATED, SendTarget.ALL_WINDOWS, {
286286
providersCount,
287287
lastUpdated: meta.lastUpdated

src/main/presenter/llmProviderPresenter/index.ts

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@ import {
1818
AcpDebugRunResult
1919
} from '@shared/presenter'
2020
import { ProviderChange, ProviderBatchUpdate } from '@shared/provider-operations'
21+
import { isProviderDbBackedProvider } from '@shared/providerDbCatalog'
2122
import { eventBus } from '@/eventbus'
22-
import { CONFIG_EVENTS } from '@/events'
23+
import { CONFIG_EVENTS, PROVIDER_DB_EVENTS } from '@/events'
2324
import { BaseLLMProvider } from './baseProvider'
2425
import { ProviderConfig, StreamState } from './types'
2526
import { RateLimitManager } from './managers/rateLimitManager'
@@ -47,6 +48,8 @@ const createAbortError = (): Error => {
4748
export class LLMProviderPresenter implements ILlmProviderPresenter {
4849
private currentProviderId: string | null = null
4950
private readonly activeStreams: Map<string, StreamState> = new Map()
51+
private readonly modelRefreshPromises: Map<string, Promise<void>> = new Map()
52+
private readonly configPresenter: IConfigPresenter
5053
private readonly config: ProviderConfig = {
5154
maxConcurrentStreams: 10
5255
}
@@ -63,6 +66,7 @@ export class LLMProviderPresenter implements ILlmProviderPresenter {
6366
sqlitePresenter: ISQLitePresenter,
6467
mcpRuntime?: ProviderMcpRuntimePort
6568
) {
69+
this.configPresenter = configPresenter
6670
this.rateLimitManager = new RateLimitManager(configPresenter)
6771
this.acpSessionPersistence = new AcpSessionPersistence(sqlitePresenter)
6872
this.providerInstanceManager = new ProviderInstanceManager({
@@ -105,6 +109,10 @@ export class LLMProviderPresenter implements ILlmProviderPresenter {
105109
eventBus.on(CONFIG_EVENTS.PROVIDER_BATCH_UPDATE, (batchUpdate: ProviderBatchUpdate) => {
106110
this.providerInstanceManager.handleProviderBatchUpdate(batchUpdate)
107111
})
112+
113+
eventBus.on(PROVIDER_DB_EVENTS.UPDATED, () => {
114+
this.refreshEnabledProviderDbBackedModelsInBackground('provider-db-updated')
115+
})
108116
}
109117

110118
getProviders(): LLM_PROVIDER[] {
@@ -378,10 +386,58 @@ export class LLMProviderPresenter implements ILlmProviderPresenter {
378386
return provider.getKeyStatus()
379387
}
380388

381-
async refreshModels(providerId: string): Promise<void> {
382-
try {
389+
private getEnabledProviderIdsUsingProviderDb(): string[] {
390+
return this.providerInstanceManager
391+
.getProviders()
392+
.filter((provider) => provider.enable && isProviderDbBackedProvider(provider.id))
393+
.map((provider) => provider.id)
394+
}
395+
396+
private async syncProviderDbBeforeRefresh(providerId: string): Promise<void> {
397+
if (!isProviderDbBackedProvider(providerId)) {
398+
return
399+
}
400+
401+
const result = await this.configPresenter.refreshProviderDb(true)
402+
if (result.status === 'error') {
403+
throw new Error(result.message || 'Provider DB refresh failed')
404+
}
405+
}
406+
407+
private enqueueProviderModelRefresh(providerId: string): Promise<void> {
408+
const existingRefresh = this.modelRefreshPromises.get(providerId)
409+
if (existingRefresh) {
410+
return existingRefresh
411+
}
412+
413+
const refreshPromise = (async () => {
383414
const provider = this.getProviderInstance(providerId)
384415
await provider.refreshModels()
416+
})().finally(() => {
417+
if (this.modelRefreshPromises.get(providerId) === refreshPromise) {
418+
this.modelRefreshPromises.delete(providerId)
419+
}
420+
})
421+
422+
this.modelRefreshPromises.set(providerId, refreshPromise)
423+
return refreshPromise
424+
}
425+
426+
private refreshEnabledProviderDbBackedModelsInBackground(reason: string): void {
427+
for (const providerId of this.getEnabledProviderIdsUsingProviderDb()) {
428+
void this.enqueueProviderModelRefresh(providerId).catch((error) => {
429+
console.warn(
430+
`[LLMProviderPresenter] Failed to refresh models for provider ${providerId} during ${reason}:`,
431+
error
432+
)
433+
})
434+
}
435+
}
436+
437+
async refreshModels(providerId: string): Promise<void> {
438+
try {
439+
await this.syncProviderDbBeforeRefresh(providerId)
440+
await this.enqueueProviderModelRefresh(providerId)
385441
} catch (error) {
386442
console.error(`Failed to refresh models for provider ${providerId}:`, error)
387443
const errorMessage = error instanceof Error ? error.message : String(error)

src/main/presenter/llmProviderPresenter/providers/doubaoProvider.ts

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,21 +16,11 @@ import {
1616
} from '@shared/modelConfigDefaults'
1717
import { OpenAICompatibleProvider } from './openAICompatibleProvider'
1818
import { providerDbLoader } from '../../configPresenter/providerDbLoader'
19-
import { modelCapabilities } from '../../configPresenter/modelCapabilities'
2019
import type { ProviderMcpRuntimePort } from '../runtimePorts'
2120

22-
export class DoubaoProvider extends OpenAICompatibleProvider {
23-
// List of models that support thinking parameter
24-
private static readonly THINKING_MODELS: string[] = [
25-
'deepseek-v3-1-250821',
26-
'doubao-seed-1-6-vision-250815',
27-
'doubao-seed-1-6-250615',
28-
'doubao-seed-1-6-flash-250615',
29-
'doubao-1-5-thinking-vision-pro-250428',
30-
'doubao-1-5-ui-tars-250428',
31-
'doubao-1-5-thinking-pro-m-250428'
32-
]
21+
const DOUBAO_THINKING_NOTE = 'doubao-thinking-parameter'
3322

23+
export class DoubaoProvider extends OpenAICompatibleProvider {
3424
constructor(
3525
provider: LLM_PROVIDER,
3626
configPresenter: IConfigPresenter,
@@ -41,7 +31,9 @@ export class DoubaoProvider extends OpenAICompatibleProvider {
4131
}
4232

4333
private supportsThinking(modelId: string): boolean {
44-
return DoubaoProvider.THINKING_MODELS.includes(modelId)
34+
const model = providerDbLoader.getModel(this.provider.id, modelId)
35+
const notes = model?.extra_capabilities?.reasoning?.notes
36+
return Array.isArray(notes) && notes.includes(DOUBAO_THINKING_NOTE)
4537
}
4638

4739
/**
@@ -93,8 +85,7 @@ export class DoubaoProvider extends OpenAICompatibleProvider {
9385
}
9486

9587
protected async fetchOpenAIModels(): Promise<MODEL_META[]> {
96-
const resolvedId = modelCapabilities.resolveProviderId(this.provider.id) || this.provider.id
97-
const provider = providerDbLoader.getProvider(resolvedId)
88+
const provider = providerDbLoader.getProvider(this.provider.id)
9889
if (!provider || !Array.isArray(provider.models)) {
9990
return []
10091
}

src/renderer/settings/components/ProviderApiConfig.vue

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,8 +165,10 @@
165165
: t('settings.provider.refreshModels')
166166
}}
167167
</Button>
168-
<!-- Key Status Display -->
169168
</div>
169+
<p v-if="shouldRefreshProviderDbFirst" class="text-xs leading-5 text-muted-foreground">
170+
{{ t('settings.provider.refreshModelsWithMetadataHint') }}
171+
</p>
170172
<div v-if="!provider.custom" class="text-xs text-muted-foreground">
171173
{{ t('settings.provider.howToGet') }}: {{ t('settings.provider.getKeyTip') }}
172174
<a :href="providerWebsites?.apiKey" target="_blank" class="text-primary">{{
@@ -193,8 +195,10 @@ import {
193195
import { Icon } from '@iconify/vue'
194196
import GitHubCopilotOAuth from './GitHubCopilotOAuth.vue'
195197
import { usePresenter } from '@/composables/usePresenter'
198+
import { useToast } from '@/components/use-toast'
196199
import { useModelCheckStore } from '@/stores/modelCheck'
197200
import type { LLM_PROVIDER, KeyStatus } from '@shared/presenter'
201+
import { isProviderDbBackedProvider } from '@shared/providerDbCatalog'
198202
199203
interface ProviderWebsites {
200204
official: string
@@ -207,6 +211,7 @@ interface ProviderWebsites {
207211
const { t } = useI18n()
208212
const llmProviderPresenter = usePresenter('llmproviderPresenter')
209213
const modelCheckStore = useModelCheckStore()
214+
const { toast } = useToast()
210215
211216
const EDITABLE_BASE_URL_PROVIDER_IDS = new Set([
212217
'openai',
@@ -247,6 +252,7 @@ const isBaseUrlEditableByDefault = computed(
247252
const showLockedBaseUrl = computed(
248253
() => !isBaseUrlEditableByDefault.value && !baseUrlUnlocked.value
249254
)
255+
const shouldRefreshProviderDbFirst = computed(() => isProviderDbBackedProvider(props.provider.id))
250256
251257
watch(
252258
() => props.provider,
@@ -323,8 +329,27 @@ const refreshModels = async () => {
323329
isRefreshing.value = true
324330
try {
325331
await llmProviderPresenter.refreshModels(props.provider.id)
332+
toast({
333+
title: t('settings.provider.toast.refreshModelsSuccessTitle'),
334+
description: t(
335+
shouldRefreshProviderDbFirst.value
336+
? 'settings.provider.toast.refreshModelsSuccessDescriptionWithMetadata'
337+
: 'settings.provider.toast.refreshModelsSuccessDescription'
338+
),
339+
duration: 4000
340+
})
326341
} catch (error) {
327342
console.error('Failed to refresh models:', error)
343+
toast({
344+
title: t('settings.provider.toast.refreshModelsFailedTitle'),
345+
description: t(
346+
shouldRefreshProviderDbFirst.value
347+
? 'settings.provider.toast.refreshModelsFailedDescriptionWithMetadata'
348+
: 'settings.provider.toast.refreshModelsFailedDescription'
349+
),
350+
variant: 'destructive',
351+
duration: 4000
352+
})
328353
} finally {
329354
isRefreshing.value = false
330355
}

src/renderer/src/i18n/da-DK/settings.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,7 @@
420420
"openaiResponsesNotice": "OpenAI bruger som standard Responses API. Hvis et tredjeparts-endpoint kun understøtter Chat Completions, skal du bruge OpenAI Completions-udbyderen.",
421421
"modifyBaseUrl": "Rediger",
422422
"baseUrlLockedHint": "Denne udbyder er låst til den anbefalede basis-URL for at mindske fejlkonfiguration.",
423+
"refreshModelsWithMetadataHint": "Opdatering af denne udbyder synkroniserer først upstream-metadata og genopbygger derefter den lokale modelliste.",
423424
"modelList": "Modelliste",
424425
"enableModels": "Aktivér modeller",
425426
"disableAllModels": "Deaktiver alle modeller",
@@ -594,7 +595,13 @@
594595
"backupSuccessTitle": "Backup fuldført",
595596
"backupSuccessMessage": "Backup gemt {time} ({size})",
596597
"importSuccessTitle": "Import fuldført",
597-
"importSuccessMessage": "Importerede {count} samtaler"
598+
"importSuccessMessage": "Importerede {count} samtaler",
599+
"refreshModelsSuccessTitle": "Modeller opdateret",
600+
"refreshModelsSuccessDescription": "Den nyeste modelliste er blevet synkroniseret for denne udbyder.",
601+
"refreshModelsSuccessDescriptionWithMetadata": "Upstream-metadata og den nyeste modelliste er blevet synkroniseret for denne udbyder.",
602+
"refreshModelsFailedTitle": "Opdatering mislykkedes",
603+
"refreshModelsFailedDescription": "Modellerne for denne udbyder kunne ikke opdateres lige nu. Prøv igen senere.",
604+
"refreshModelsFailedDescriptionWithMetadata": "Upstream-metadata og modeller for denne udbyder kunne ikke opdateres lige nu. Prøv igen senere."
598605
},
599606
"modelscope": {
600607
"mcpSync": {

src/renderer/src/i18n/en-US/settings.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -548,6 +548,7 @@
548548
"openaiResponsesNotice": "OpenAI defaults to the Responses API. If a third-party endpoint only supports Chat Completions, use the OpenAI Completions provider.",
549549
"modifyBaseUrl": "Modify",
550550
"baseUrlLockedHint": "This provider is pinned to the recommended Base URL to reduce misconfiguration.",
551+
"refreshModelsWithMetadataHint": "Refreshing this provider will sync upstream metadata first, then rebuild the local model list.",
551552
"modelList": "Model List",
552553
"enableModels": "Enable Models",
553554
"disableAllModels": "Disable All Models",
@@ -690,7 +691,13 @@
690691
"backupSuccessTitle": "Backup completed",
691692
"backupSuccessMessage": "Backup saved at {time} ({size})",
692693
"importSuccessTitle": "Import completed",
693-
"importSuccessMessage": "Successfully imported {count} conversations"
694+
"importSuccessMessage": "Successfully imported {count} conversations",
695+
"refreshModelsSuccessTitle": "Models refreshed",
696+
"refreshModelsSuccessDescription": "The latest model list has been synced for this provider.",
697+
"refreshModelsSuccessDescriptionWithMetadata": "Upstream metadata and the latest model list have been synced for this provider.",
698+
"refreshModelsFailedTitle": "Refresh failed",
699+
"refreshModelsFailedDescription": "Unable to refresh models for this provider right now. Please try again later.",
700+
"refreshModelsFailedDescriptionWithMetadata": "Unable to refresh upstream metadata and models for this provider right now. Please try again later."
694701
},
695702
"modelscope": {
696703
"mcpSync": {

src/renderer/src/i18n/fa-IR/settings.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -486,6 +486,7 @@
486486
"openaiResponsesNotice": "OpenAI به صورت پیش‌فرض از Responses API استفاده می‌کند. اگر endpoint شخص ثالث فقط از Chat Completions پشتیبانی می‌کند، از ارائه‌دهنده OpenAI Completions استفاده کنید.",
487487
"modifyBaseUrl": "ویرایش",
488488
"baseUrlLockedHint": "این ارائه‌دهنده برای کاهش خطاهای پیکربندی روی آدرس پایه پیشنهادی قفل شده است.",
489+
"refreshModelsWithMetadataHint": "نوسازی این ارائه‌دهنده ابتدا فرادادهٔ بالادستی را همگام می‌کند و سپس فهرست محلی مدل‌ها را بازسازی می‌کند.",
489490
"modelList": "فهرست مدل‌ها",
490491
"enableModels": "روشن کردن مدل‌ها",
491492
"disableAllModels": "خاموش کردن همه مدل‌ها",
@@ -660,7 +661,13 @@
660661
"backupSuccessTitle": "پشتیبان‌گیری کامل شد",
661662
"backupSuccessMessage": "فایل پشتیبان در {time} ذخیره شد (حجم: {size})",
662663
"importSuccessTitle": "درون‌ریزی با موفقیت انجام شد",
663-
"importSuccessMessage": "{count} گفتگو با موفقیت درون‌ریزی شد"
664+
"importSuccessMessage": "{count} گفتگو با موفقیت درون‌ریزی شد",
665+
"refreshModelsSuccessTitle": "مدل‌ها نوسازی شدند",
666+
"refreshModelsSuccessDescription": "جدیدترین فهرست مدل برای این ارائه‌دهنده همگام شد.",
667+
"refreshModelsSuccessDescriptionWithMetadata": "فرادادهٔ بالادستی و جدیدترین فهرست مدل برای این ارائه‌دهنده همگام شد.",
668+
"refreshModelsFailedTitle": "نوسازی ناموفق بود",
669+
"refreshModelsFailedDescription": "در حال حاضر امکان نوسازی مدل‌های این ارائه‌دهنده وجود ندارد. لطفاً بعداً دوباره تلاش کنید.",
670+
"refreshModelsFailedDescriptionWithMetadata": "در حال حاضر امکان همگام‌سازی فرادادهٔ بالادستی و مدل‌های این ارائه‌دهنده وجود ندارد. لطفاً بعداً دوباره تلاش کنید."
664671
},
665672
"apiKeyLabel": "کلید API",
666673
"apiUrlLabel": "آدرس API",

src/renderer/src/i18n/fr-FR/settings.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -486,6 +486,7 @@
486486
"openaiResponsesNotice": "OpenAI utilise par défaut l'API Responses. Si un endpoint tiers ne prend en charge que Chat Completions, utilisez le fournisseur OpenAI Completions.",
487487
"modifyBaseUrl": "Modifier",
488488
"baseUrlLockedHint": "Ce fournisseur est verrouillé sur l’URL de base recommandée afin de réduire les erreurs de configuration.",
489+
"refreshModelsWithMetadataHint": "L’actualisation de ce fournisseur synchronise d’abord les métadonnées en amont, puis reconstruit la liste locale des modèles.",
489490
"modelList": "Liste des modèles",
490491
"enableModels": "Activer les modèles",
491492
"disableAllModels": "Désactiver tous les modèles",
@@ -660,7 +661,13 @@
660661
"backupSuccessTitle": "Sauvegarde terminée",
661662
"backupSuccessMessage": "Sauvegarde enregistrée à {time} ({size})",
662663
"importSuccessTitle": "Importation terminée",
663-
"importSuccessMessage": "{count} conversations importées avec succès"
664+
"importSuccessMessage": "{count} conversations importées avec succès",
665+
"refreshModelsSuccessTitle": "Modèles actualisés",
666+
"refreshModelsSuccessDescription": "La dernière liste de modèles a été synchronisée pour ce fournisseur.",
667+
"refreshModelsSuccessDescriptionWithMetadata": "Les métadonnées en amont et la dernière liste de modèles ont été synchronisées pour ce fournisseur.",
668+
"refreshModelsFailedTitle": "Actualisation échouée",
669+
"refreshModelsFailedDescription": "Impossible d’actualiser les modèles de ce fournisseur pour le moment. Veuillez réessayer plus tard.",
670+
"refreshModelsFailedDescriptionWithMetadata": "Impossible de synchroniser les métadonnées en amont et les modèles de ce fournisseur pour le moment. Veuillez réessayer plus tard."
664671
},
665672
"apiKeyLabel": "Clé API",
666673
"apiUrlLabel": "URL API",

src/renderer/src/i18n/he-IL/settings.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -486,6 +486,7 @@
486486
"openaiResponsesNotice": "OpenAI משתמשת כברירת מחדל ב-Responses API. אם endpoint של צד שלישי תומך רק ב-Chat Completions, השתמש בספק OpenAI Completions.",
487487
"modifyBaseUrl": "ערוך",
488488
"baseUrlLockedHint": "הספק הזה נעול לכתובת הבסיס המומלצת כדי לצמצם שגיאות תצורה.",
489+
"refreshModelsWithMetadataHint": "רענון הספק הזה יסנכרן קודם את המטא־דאטה במעלה הזרם ולאחר מכן יבנה מחדש את רשימת המודלים המקומית.",
489490
"modelList": "רשימת מודלים",
490491
"enableModels": "הפעל מודלים",
491492
"disableAllModels": "השבת את כל המודלים",
@@ -660,7 +661,13 @@
660661
"backupSuccessTitle": "הגיבוי הושלם",
661662
"backupSuccessMessage": "הגיבוי נשמר ב-{time} ({size})",
662663
"importSuccessTitle": "הייבוא הושלם",
663-
"importSuccessMessage": "יובאו בהצלחה {count} שיחות"
664+
"importSuccessMessage": "יובאו בהצלחה {count} שיחות",
665+
"refreshModelsSuccessTitle": "המודלים רועננו",
666+
"refreshModelsSuccessDescription": "רשימת המודלים העדכנית סונכרנה עבור הספק הזה.",
667+
"refreshModelsSuccessDescriptionWithMetadata": "המטא־דאטה במעלה הזרם ורשימת המודלים העדכנית סונכרנו עבור הספק הזה.",
668+
"refreshModelsFailedTitle": "הרענון נכשל",
669+
"refreshModelsFailedDescription": "לא ניתן לרענן כעת את המודלים של הספק הזה. נסה שוב מאוחר יותר.",
670+
"refreshModelsFailedDescriptionWithMetadata": "לא ניתן לסנכרן כעת את המטא־דאטה במעלה הזרם ואת המודלים של הספק הזה. נסה שוב מאוחר יותר."
664671
},
665672
"modelscope": {
666673
"mcpSync": {

0 commit comments

Comments
 (0)