Skip to content

fix(provider): sync db-backed model lists#1432

Merged
zerob13 merged 2 commits intodevfrom
model-list
Apr 7, 2026
Merged

fix(provider): sync db-backed model lists#1432
zerob13 merged 2 commits intodevfrom
model-list

Conversation

@zhangmo8
Copy link
Copy Markdown
Collaborator

@zhangmo8 zhangmo8 commented Apr 7, 2026

Summary by CodeRabbit

Release Notes

  • New Features

    • Added automatic upstream metadata synchronization when refreshing models for select providers, ensuring local model lists stay current with upstream catalogs.
    • Implemented deduplication of concurrent model refresh requests to prevent redundant operations.
    • Enhanced UI with helpful hints and success/failure notifications for model refresh operations.
    • Extended reasoning capability detection for supported AI models.
  • Documentation

    • Added localized UI text across 12 languages for metadata sync hints and refresh notifications.
  • Tests

    • Added comprehensive test coverage for background model synchronization and provider refresh behavior.

@zhangmo8 zhangmo8 requested a review from zerob13 April 7, 2026 01:30
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 7, 2026

📝 Walkthrough

Walkthrough

The PR implements a provider-database-backed model refresh system with background synchronization. It adds a listener for database updates that triggers conditional model refreshes for database-backed providers, introduces deduplication of concurrent refresh requests, and includes conditional UI messaging with toast notifications. The Doubao provider's thinking-model detection migrates from a hardcoded list to database lookups. A new utility module identifies which providers are database-backed.

Changes

Cohort / File(s) Summary
Provider DB Loading
src/main/presenter/configPresenter/providerDbLoader.ts
Changed providersCount computation to derive from cached state (this.cache.providers) rather than local sanitized variable after refresh.
LLM Provider Presenter
src/main/presenter/llmProviderPresenter/index.ts
Added listener for PROVIDER_DB_EVENTS.UPDATED triggering background model refresh for DB-backed providers. Introduced modelRefreshPromises map with deduplication helpers to prevent concurrent refresh requests. Modified refreshModels() to conditionally call configPresenter.refreshProviderDb(true) for DB-backed providers before model refresh.
Doubao Provider
src/main/presenter/llmProviderPresenter/providers/doubaoProvider.ts
Removed hardcoded thinking-model list. Replaced with dynamic lookup via providerDbLoader checking extra_capabilities.reasoning.notes for reasoning capability markers.
Provider DB Catalog Utility
src/shared/providerDbCatalog.ts
New module exporting isProviderDbBackedProvider() function that identifies database-backed providers (doubao, zhipu, minimax, o3fan) with falsy-safe input handling.
Provider API Configuration UI
src/renderer/settings/components/ProviderApiConfig.vue
Added conditional UX for DB-backed providers: displays metadata refresh hint, uses provider-category-specific i18n keys for toast descriptions, and shows success/failure toast notifications with destructive styling on errors.
Internationalization
src/renderer/src/i18n/*/settings.json (11 files)
Added refreshModelsWithMetadataHint hint text and extended provider.toast with success/failure title/description variants (with and without metadata context) across all supported languages: da-DK, en-US, fa-IR, fr-FR, he-IL, ja-JP, ko-KR, pt-BR, ru-RU, zh-CN, zh-HK, zh-TW.
Background Model Sync Tests
test/main/presenter/llmProviderPresenter/backgroundModelSync.test.ts
Comprehensive test suite validating background synchronization behavior: verifies non-DB-backed providers skip refresh, DB-backed providers refresh on provider-db:updated events, concurrent updates coalesce, refreshProviderDb is called before provider model refresh for DB-backed providers, and failure handling surfaces errors appropriately.
Doubao Provider Tests
test/main/presenter/llmProviderPresenter/doubaoProvider.test.ts
Test suite for Doubao provider functionality: validates model-to-internal-object mapping and verifies supportsThinking() detection via provider DB lookup with correct thinking parameter inclusion in OpenAI API calls.
Provider API Config Tests
test/renderer/components/ProviderApiConfig.test.ts
Test coverage for conditional refresh UI behavior: verifies metadata hint rendering, toast notifications with appropriate descriptions for DB-backed vs. non-DB-backed providers, and error handling with destructive toast styling.

Sequence Diagram(s)

sequenceDiagram
    participant EB as Event Bus
    participant Presenter as LLMProviderPresenter
    participant CP as ConfigPresenter
    participant DB as ProviderDB
    participant Provider as OpenAICompatibleProvider
    participant UI as ProviderApiConfig.vue

    Note over EB,UI: Background DB-driven refresh flow
    EB->>Presenter: PROVIDER_DB_EVENTS.UPDATED
    Presenter->>Presenter: Filter enabled DB-backed providers
    Presenter->>Presenter: Enqueue refresh (deduplicate)
    
    Note over CP,DB: Sync upstream metadata
    Presenter->>CP: refreshProviderDb(true)
    CP->>DB: Sync providers metadata
    DB-->>CP: Status response
    CP-->>Presenter: Return status
    
    alt refreshProviderDb success
        Presenter->>Provider: refreshModels()
        Provider->>DB: Fetch models
        DB-->>Provider: Model list
        Provider-->>Presenter: Resolved
    else refreshProviderDb error
        Presenter-->>UI: Reject with error
        UI->>UI: Show destructive toast
    end
    
    Note over Presenter,UI: User-initiated refresh flow
    UI->>Presenter: refreshModels(providerId)
    Presenter->>Presenter: Check if DB-backed
    alt isProviderDbBackedProvider
        Presenter->>CP: refreshProviderDb(true)
        CP-->>Presenter: Status
        Presenter->>Provider: refreshModels()
    else non-DB provider
        Presenter->>Provider: refreshModels()
    end
    Provider-->>Presenter: Resolved
    Presenter-->>UI: Success
    UI->>UI: Show success toast (with/without metadata)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~55 minutes

Possibly related PRs

Poem

🐰 A whisker-twitch of joy—
Database-backed refresh takes flight,
Doubao thinks with grace,
Toasts pop in every tongue,
Models bloom anew! 🌱

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix(provider): sync db-backed model lists' directly relates to the main change: adding synchronization of provider database-backed model lists before refresh operations, as evidenced by the listener for PROVIDER_DB_EVENTS.UPDATED and the new DB refresh logic.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch model-list

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/renderer/settings/components/ProviderApiConfig.vue (1)

326-352: ⚠️ Potential issue | 🟡 Minor

Snapshot the provider classification before awaiting the refresh.

If the parent reuses this component instance for another provider while the request is in flight, Lines 335-349 can pick up the new props.provider.id state and show the wrong success/failure copy for the refresh that just finished.

💡 Suggested fix
 const refreshModels = async () => {
   if (isRefreshing.value) return

+  const providerId = props.provider.id
+  const refreshesMetadataFirst = shouldRefreshProviderDbFirst.value
   isRefreshing.value = true
   try {
-    await llmProviderPresenter.refreshModels(props.provider.id)
+    await llmProviderPresenter.refreshModels(providerId)
     toast({
       title: t('settings.provider.toast.refreshModelsSuccessTitle'),
       description: t(
-        shouldRefreshProviderDbFirst.value
+        refreshesMetadataFirst
           ? 'settings.provider.toast.refreshModelsSuccessDescriptionWithMetadata'
           : 'settings.provider.toast.refreshModelsSuccessDescription'
       ),
       duration: 4000
     })
   } catch (error) {
     console.error('Failed to refresh models:', error)
     toast({
       title: t('settings.provider.toast.refreshModelsFailedTitle'),
       description: t(
-        shouldRefreshProviderDbFirst.value
+        refreshesMetadataFirst
           ? 'settings.provider.toast.refreshModelsFailedDescriptionWithMetadata'
           : 'settings.provider.toast.refreshModelsFailedDescription'
       ),
       variant: 'destructive',
       duration: 4000
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/settings/components/ProviderApiConfig.vue` around lines 326 -
352, Snapshot the provider identity and the classification flag before the await
in refreshModels: capture props.provider.id (e.g., providerId) and
shouldRefreshProviderDbFirst.value (e.g., snapshotShouldRefresh) at the top of
refreshModels, then call llmProviderPresenter.refreshModels(providerId) and use
snapshotShouldRefresh for selecting the toast description strings and in the
success/failure logic so the messages correspond to the request that completed
rather than a possibly changed props.provider; keep isRefreshing handling as-is.
🧹 Nitpick comments (3)
test/main/presenter/llmProviderPresenter/doubaoProvider.test.ts (2)

179-218: Add the negative case for models without the Doubao thinking note.

This only locks the positive branch. If coreStream starts sending thinking for every Doubao model, this suite would still pass even though unflagged models may reject the request.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/main/presenter/llmProviderPresenter/doubaoProvider.test.ts` around lines
179 - 218, The test only asserts the positive branch where a model has the
'doubao-thinking-parameter' note; add a negative case to ensure models without
that note do not send the thinking parameter. Create another test (or extend
this one) that mocks mockGetModel to return a model without
extra_capabilities.reasoning.notes, call DoubaoProvider.coreStream (as done
here), collect events, and assert mockChatCompletionsCreate was called with an
objectContaining that does NOT include a thinking property (or that thinking is
undefined), referencing DoubaoProvider, coreStream, mockGetModel, and
mockChatCompletionsCreate to locate the code to change.

1-4: Mirror the providers/ subdirectory in this test path.

This suite targets src/main/presenter/llmProviderPresenter/providers/doubaoProvider.ts, so it should live under test/main/presenter/llmProviderPresenter/providers/ as well.

As per coding guidelines, "Vitest test suites should mirror the source structure under test/main/** and test/renderer/**".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/main/presenter/llmProviderPresenter/doubaoProvider.test.ts` around lines
1 - 4, The test is placed at test/main/presenter/llmProviderPresenter/ but must
mirror the source providers subdirectory; move the test so it lives alongside
the implementation’s subfolder (i.e., under a providers/ folder corresponding to
the implementation) and update the module imports accordingly so they correctly
import DoubaoProvider and any types (IConfigPresenter, LLM_PROVIDER,
ModelConfig) from the same relative locations as the source file; ensure the
test filename remains doubaoProvider.test.ts and update any import paths in that
file to reflect the new providers/ location.
test/main/presenter/llmProviderPresenter/backgroundModelSync.test.ts (1)

204-247: Cover the disabled DB-backed provider case too.

All provider-db:updated coverage here uses an enabled DB-backed provider. A disabled DB-backed provider should also ignore the event, otherwise catalog updates can still trigger background refresh work for a provider the user turned off.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/main/presenter/llmProviderPresenter/backgroundModelSync.test.ts` around
lines 204 - 247, Add a test that mirrors the enabled DB-backed case but uses a
DB-backed provider marked disabled so provider-db:updated is ignored: spy on
OpenAICompatibleProvider.prototype.refreshModels, instantiate
LLMProviderPresenter with createConfigPresenter(createProvider({ id: 'doubao',
apiType: 'doubao', /* mark provider disabled e.g. enabled: false or dbEnabled:
false */ })), await the microtasks as in the other tests,
emitMainEvent('provider-db:updated', {...}), and assert
refreshSpy.not.toHaveBeenCalled(); this ensures LLMProviderPresenter ignores
provider-db updates for DB-backed providers that the user has disabled.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/presenter/llmProviderPresenter/index.ts`:
- Around line 51-52: The current coalescing uses modelRefreshPromises to
collapse concurrent refreshes into the first in-flight promise and drops any
newer requests; change this so that when a provider.refreshModels() is already
in-flight you record that a trailing refresh was requested (e.g., set a pending
flag or increment a counter keyed by provider id), and when the in-flight
promise resolves you check that pending indicator and, if set, invoke
provider.refreshModels() again (updating modelRefreshPromises to the new promise
and clearing the pending flag); reference the existing modelRefreshPromises map
and the calls to provider.refreshModels() so the map is updated with each actual
run and newer refresh requests are not discarded.

In `@test/main/presenter/llmProviderPresenter/backgroundModelSync.test.ts`:
- Around line 172-177: The emitMainEvent helper currently fires handlers with
handlers.forEach(...) and then flushes two microtasks; replace that with
collecting handler results and awaiting them so async handlers settle
deterministically: in emitMainEvent (and using
eventState.handlers.get(eventName)), map handlers to their returned values (cast
to Promise<unknown> if needed), call Promise.all on that array and await it
before returning, ensuring both sync and async listeners are properly awaited
instead of relying on Promise.resolve() hops.

---

Outside diff comments:
In `@src/renderer/settings/components/ProviderApiConfig.vue`:
- Around line 326-352: Snapshot the provider identity and the classification
flag before the await in refreshModels: capture props.provider.id (e.g.,
providerId) and shouldRefreshProviderDbFirst.value (e.g., snapshotShouldRefresh)
at the top of refreshModels, then call
llmProviderPresenter.refreshModels(providerId) and use snapshotShouldRefresh for
selecting the toast description strings and in the success/failure logic so the
messages correspond to the request that completed rather than a possibly changed
props.provider; keep isRefreshing handling as-is.

---

Nitpick comments:
In `@test/main/presenter/llmProviderPresenter/backgroundModelSync.test.ts`:
- Around line 204-247: Add a test that mirrors the enabled DB-backed case but
uses a DB-backed provider marked disabled so provider-db:updated is ignored: spy
on OpenAICompatibleProvider.prototype.refreshModels, instantiate
LLMProviderPresenter with createConfigPresenter(createProvider({ id: 'doubao',
apiType: 'doubao', /* mark provider disabled e.g. enabled: false or dbEnabled:
false */ })), await the microtasks as in the other tests,
emitMainEvent('provider-db:updated', {...}), and assert
refreshSpy.not.toHaveBeenCalled(); this ensures LLMProviderPresenter ignores
provider-db updates for DB-backed providers that the user has disabled.

In `@test/main/presenter/llmProviderPresenter/doubaoProvider.test.ts`:
- Around line 179-218: The test only asserts the positive branch where a model
has the 'doubao-thinking-parameter' note; add a negative case to ensure models
without that note do not send the thinking parameter. Create another test (or
extend this one) that mocks mockGetModel to return a model without
extra_capabilities.reasoning.notes, call DoubaoProvider.coreStream (as done
here), collect events, and assert mockChatCompletionsCreate was called with an
objectContaining that does NOT include a thinking property (or that thinking is
undefined), referencing DoubaoProvider, coreStream, mockGetModel, and
mockChatCompletionsCreate to locate the code to change.
- Around line 1-4: The test is placed at
test/main/presenter/llmProviderPresenter/ but must mirror the source providers
subdirectory; move the test so it lives alongside the implementation’s subfolder
(i.e., under a providers/ folder corresponding to the implementation) and update
the module imports accordingly so they correctly import DoubaoProvider and any
types (IConfigPresenter, LLM_PROVIDER, ModelConfig) from the same relative
locations as the source file; ensure the test filename remains
doubaoProvider.test.ts and update any import paths in that file to reflect the
new providers/ location.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4242595a-da82-4ae3-ad2c-e1c8e2730e33

📥 Commits

Reviewing files that changed from the base of the PR and between 7f1e1b1 and 500dea4.

📒 Files selected for processing (20)
  • src/main/presenter/configPresenter/providerDbLoader.ts
  • src/main/presenter/llmProviderPresenter/index.ts
  • src/main/presenter/llmProviderPresenter/providers/doubaoProvider.ts
  • src/renderer/settings/components/ProviderApiConfig.vue
  • src/renderer/src/i18n/da-DK/settings.json
  • src/renderer/src/i18n/en-US/settings.json
  • src/renderer/src/i18n/fa-IR/settings.json
  • src/renderer/src/i18n/fr-FR/settings.json
  • src/renderer/src/i18n/he-IL/settings.json
  • src/renderer/src/i18n/ja-JP/settings.json
  • src/renderer/src/i18n/ko-KR/settings.json
  • src/renderer/src/i18n/pt-BR/settings.json
  • src/renderer/src/i18n/ru-RU/settings.json
  • src/renderer/src/i18n/zh-CN/settings.json
  • src/renderer/src/i18n/zh-HK/settings.json
  • src/renderer/src/i18n/zh-TW/settings.json
  • src/shared/providerDbCatalog.ts
  • test/main/presenter/llmProviderPresenter/backgroundModelSync.test.ts
  • test/main/presenter/llmProviderPresenter/doubaoProvider.test.ts
  • test/renderer/components/ProviderApiConfig.test.ts

Comment on lines +51 to +52
private readonly modelRefreshPromises: Map<string, Promise<void>> = new Map()
private readonly configPresenter: IConfigPresenter
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don't drop newer refresh requests behind an older in-flight refresh.

Lines 408-410 collapse every concurrent request for the same provider into the first promise and never schedule a trailing rerun. If another provider-db update lands while the first provider.refreshModels() is already reading the previous snapshot, that later refresh is discarded and the local model list can settle on stale data.

♻️ Suggested coalescing pattern
 export class LLMProviderPresenter implements ILlmProviderPresenter {
   private currentProviderId: string | null = null
   private readonly activeStreams: Map<string, StreamState> = new Map()
   private readonly modelRefreshPromises: Map<string, Promise<void>> = new Map()
+  private readonly pendingModelRefreshes: Set<string> = new Set()
   private readonly configPresenter: IConfigPresenter
 ...
   private enqueueProviderModelRefresh(providerId: string): Promise<void> {
     const existingRefresh = this.modelRefreshPromises.get(providerId)
     if (existingRefresh) {
+      this.pendingModelRefreshes.add(providerId)
       return existingRefresh
     }

-    const refreshPromise = (async () => {
-      const provider = this.getProviderInstance(providerId)
-      await provider.refreshModels()
-    })().finally(() => {
+    const runRefresh = async (): Promise<void> => {
+      do {
+        this.pendingModelRefreshes.delete(providerId)
+        const provider = this.getProviderInstance(providerId)
+        await provider.refreshModels()
+      } while (this.pendingModelRefreshes.has(providerId))
+    }
+
+    const refreshPromise = runRefresh().finally(() => {
+      this.pendingModelRefreshes.delete(providerId)
       if (this.modelRefreshPromises.get(providerId) === refreshPromise) {
         this.modelRefreshPromises.delete(providerId)
       }
     })

     this.modelRefreshPromises.set(providerId, refreshPromise)
     return refreshPromise
   }

A regression where a second refresh request arrives before the first promise resolves would make this behavior much easier to lock down.

Also applies to: 407-423

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/presenter/llmProviderPresenter/index.ts` around lines 51 - 52, The
current coalescing uses modelRefreshPromises to collapse concurrent refreshes
into the first in-flight promise and drops any newer requests; change this so
that when a provider.refreshModels() is already in-flight you record that a
trailing refresh was requested (e.g., set a pending flag or increment a counter
keyed by provider id), and when the in-flight promise resolves you check that
pending indicator and, if set, invoke provider.refreshModels() again (updating
modelRefreshPromises to the new promise and clearing the pending flag);
reference the existing modelRefreshPromises map and the calls to
provider.refreshModels() so the map is updated with each actual run and newer
refresh requests are not discarded.

Comment on lines +172 to +177
const emitMainEvent = async (eventName: string, ...args: unknown[]) => {
const handlers = eventState.handlers.get(eventName) ?? []
handlers.forEach((handler) => handler(...args))
await Promise.resolve()
await Promise.resolve()
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Await the registered handler promises instead of flushing two microtasks.

This helper currently assumes the background work settles after two Promise.resolve() hops. That makes the suite race-prone as soon as the listener adds another await.

♻️ Suggested change
 const emitMainEvent = async (eventName: string, ...args: unknown[]) => {
   const handlers = eventState.handlers.get(eventName) ?? []
-  handlers.forEach((handler) => handler(...args))
-  await Promise.resolve()
-  await Promise.resolve()
+  await Promise.all(
+    handlers.map((handler) => Promise.resolve().then(() => handler(...args)))
+  )
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/main/presenter/llmProviderPresenter/backgroundModelSync.test.ts` around
lines 172 - 177, The emitMainEvent helper currently fires handlers with
handlers.forEach(...) and then flushes two microtasks; replace that with
collecting handler results and awaiting them so async handlers settle
deterministically: in emitMainEvent (and using
eventState.handlers.get(eventName)), map handlers to their returned values (cast
to Promise<unknown> if needed), call Promise.all on that array and await it
before returning, ensuring both sync and async listeners are properly awaited
instead of relying on Promise.resolve() hops.

@zerob13 zerob13 merged commit 4642abb into dev Apr 7, 2026
3 checks passed
@zhangmo8 zhangmo8 deleted the model-list branch April 7, 2026 05:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants