Skip to content

fix(model-list): add model fetching with fallback for Anthropic an…#1560

Merged
zhangmo8 merged 1 commit into
devfrom
feature/llm-provider-updates
Apr 29, 2026
Merged

fix(model-list): add model fetching with fallback for Anthropic an…#1560
zhangmo8 merged 1 commit into
devfrom
feature/llm-provider-updates

Conversation

@zhangmo8

@zhangmo8 zhangmo8 commented Apr 29, 2026

Copy link
Copy Markdown
Collaborator

…d Gemini

  • Add suppressErrors option to fetchModels in baseProvider
  • Implement fetchAnthropicModelsWithFallback and fetchGeminiModelsWithFallback
  • Update ProviderApiConfig component to handle new provider types
  • Add comprehensive tests for Anthropic and Gemini providers
  • Add tests for ProviderApiConfig component

Summary by CodeRabbit

  • New Features

    • Added live model discovery from Anthropic and Gemini APIs with fallback to cached configurations when unavailable
  • Bug Fixes

    • Enhanced error messaging during model refresh to display specific API error details instead of generic messages
  • Tests

    • Added comprehensive test coverage for live model discovery and error propagation

…d Gemini

- Add suppressErrors option to fetchModels in baseProvider
- Implement fetchAnthropicModelsWithFallback and fetchGeminiModelsWithFallback
- Update ProviderApiConfig component to handle new provider types
- Add comprehensive tests for Anthropic and Gemini providers
- Add tests for ProviderApiConfig component
@coderabbitai

coderabbitai Bot commented Apr 29, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Changes introduce an optional error suppression parameter to fetchModels, implement runtime-aware model fetching from Anthropic and Gemini APIs with config-db fallback, and enhance UI error display to surface underlying error details instead of generic messages.

Changes

Cohort / File(s) Summary
Base Provider Error Handling
src/main/presenter/llmProviderPresenter/baseProvider.ts
Added optional suppressErrors parameter to fetchModels() (defaults to true). refreshModels() now explicitly passes suppressErrors: false to force error propagation.
Runtime-Aware Model Fetching
src/main/presenter/llmProviderPresenter/providers/aiSdkProvider.ts
Replaced direct config-db lookups with new fetchConfigDbModels() logic that conditionally fetches live model lists from Anthropic (/models) or Gemini (/models) APIs based on runtimeKind, with fallback to config-db on fetch failure (unless custom provider). Includes request normalization, header/version handling, response parsing, and MODEL_META mapping.
UI Error Display & Presenter Config
src/renderer/settings/components/ProviderApiConfig.vue
Initialized useLegacyPresenter with safeCall: false to propagate errors. Updated toast to extract and append underlying error message (from error.message or message JSON fields) to i18n-based description when available.
Provider Tests
test/main/presenter/llmProviderPresenter/anthropicProvider.test.ts, test/main/presenter/llmProviderPresenter/geminiProvider.test.ts
Added test coverage for remote model discovery from Anthropic and Gemini APIs, including request validation (URL, headers, auth), response parsing, model list transformation, and error propagation assertions.
Component Error Handling Tests
test/renderer/components/ProviderApiConfig.test.ts
Updated mocks to support safeCall option in useLegacyPresenter. Added assertions for extracted error messages in toast descriptions and verified error propagation through safeCall: false.

Sequence Diagram(s)

sequenceDiagram
    participant UI as Component<br/>(ProviderApiConfig)
    participant Presenter as BaseProvider<br/>(fetchModels)
    participant Strategy as AiSdkProvider<br/>(fetchProviderModelsByStrategy)
    participant API as Remote API<br/>(Anthropic/Gemini)
    participant ConfigDB as Config DB

    UI->>Presenter: refreshModels()<br/>(safeCall: false)
    Presenter->>Presenter: fetchModels<br/>({ suppressErrors: false })
    Presenter->>Strategy: fetchProviderModelsByStrategy('config-db')
    
    alt Custom Provider with API Key
        Strategy->>API: GET /models<br/>(runtime-specific headers)
        alt Fetch Success
            API-->>Strategy: model list response
            Strategy->>Strategy: normalize & map to MODEL_META
            Strategy-->>Presenter: models array
        else Fetch Failure
            API-->>Strategy: error response
            Strategy->>Strategy: extract error message
            Strategy-->>Presenter: throw error
        end
    else No API Key or Fallback
        Strategy->>ConfigDB: lookup models
        ConfigDB-->>Strategy: config-db models
        Strategy-->>Presenter: models array
    end
    
    alt Success
        Presenter-->>UI: MODEL_META[]
        UI->>UI: display models
    else Error & suppressErrors: false
        Presenter-->>UI: throw error
        UI->>UI: extract error message
        UI->>UI: show toast<br/>with error details
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • zerob13

Poem

🐰 A rabbit hops through model lists so bright,
Fetching from Anthropic and Gemini's light!
Errors now whisper their secrets clear,
No more silent failures to fear—
Config-db catches when APIs aren't near! 🌟

🚥 Pre-merge checks | ✅ 4 | ❌ 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 (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main changes: adding model fetching with fallback functionality for Anthropic and Gemini providers.

✏️ 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 feature/llm-provider-updates

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.

@coderabbitai coderabbitai Bot left a comment

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.

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/main/presenter/llmProviderPresenter/baseProvider.ts (1)

238-267: ⚠️ Potential issue | 🔴 Critical

Async rejections bypass error handling in fetchModels().

Returning this.fetchProviderModels().then(...) at line 241 causes rejections to skip the catch block entirely, making the suppressErrors option ineffective and preventing error logging.

✅ Suggested fix
   public async fetchModels(options?: { suppressErrors?: boolean }): Promise<MODEL_META[]> {
     const suppressErrors = options?.suppressErrors ?? true
     try {
-      return this.fetchProviderModels().then((models) => {
+      const models = await this.fetchProviderModels()
         logger.info(
           `[Provider] fetchModels: fetched ${models?.length || 0} models for provider "${this.provider.id}"`
         )
         // Validate that all models have correct providerId
         const validatedModels = models.map((model) => {
@@
         })
         this.models = validatedModels
         this.configPresenter.setProviderModels(this.provider.id, validatedModels)
         return validatedModels
-      })
     } catch (e) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/presenter/llmProviderPresenter/baseProvider.ts` around lines 238 -
267, The current fetchModels method returns
this.fetchProviderModels().then(...), which lets promise rejections bypass the
surrounding try/catch and breaks suppressErrors and logging; change the
implementation to await the provider call (e.g. const models = await
this.fetchProviderModels()) inside the try block, keep the same
validation/assignment logic (models -> validatedModels -> this.models and
this.configPresenter.setProviderModels(this.provider.id, validatedModels)), and
in the catch block log the error via logger.error and rethrow only when
options?.suppressErrors is false to preserve the original behavior.
🧹 Nitpick comments (1)
test/main/presenter/llmProviderPresenter/anthropicProvider.test.ts (1)

171-228: Restore fetch globals between tests to avoid leakage.

These tests stub global.fetch but never un-stub it. Add cleanup in afterEach to prevent cross-test coupling.

♻️ Suggested cleanup
   afterEach(() => {
+    vi.unstubAllGlobals()
     if (originalEnvKey === undefined) {
       delete process.env.ANTHROPIC_API_KEY
       return
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/main/presenter/llmProviderPresenter/anthropicProvider.test.ts` around
lines 171 - 228, Add an afterEach cleanup to restore the stubbed global fetch
between tests: the tests currently call vi.stubGlobal('fetch') (see occurrences
in anthropicProvider.test.ts around the AiSdkProvider/createProvider tests) and
never restore it, so add an afterEach that calls vi.restoreAllMocks() and
removes or resets global.fetch so subsequent tests don't leak the stubbed fetch;
ensure the cleanup runs in this test file so both the successful fetch
(fetchModels) and failing fetch (refreshModels) tests are isolated.
🤖 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/providers/aiSdkProvider.ts`:
- Around line 894-895: Both fetchAnthropicModelsWithFallback and
fetchGeminiModelsWithFallback call this.createModelRequestSignal(null) which
leaves fetches unbounded; change both calls to pass a sensible timeout (e.g., a
configured ms value or a constant) instead of null, ensure the returned signal
is wired into the fetch request options and that dispose() is still called in
finally to cancel on timeout; update other identical call sites (the second
occurrence around the other fetch path) to match and use the same timeout
constant or config value so remote model fetches are protected from hanging.

In `@src/renderer/settings/components/ProviderApiConfig.vue`:
- Around line 328-354: extractRefreshErrorMessage currently converts non-Error
rejections to String(error) which yields “[object Object]” and loses message
fields; update the function (extractRefreshErrorMessage) to first detect
plain-object rejections (e.g., typeof error === 'object' && error !== null) and
extract a string from error.message or error.error?.message when present (trim
and validate), then fall back to the Error instance message or String(error)
only if no message field exists; keep the existing JSON.parse attempt for
stringified objects and preserve the final null/normalizedMessage behavior.

---

Outside diff comments:
In `@src/main/presenter/llmProviderPresenter/baseProvider.ts`:
- Around line 238-267: The current fetchModels method returns
this.fetchProviderModels().then(...), which lets promise rejections bypass the
surrounding try/catch and breaks suppressErrors and logging; change the
implementation to await the provider call (e.g. const models = await
this.fetchProviderModels()) inside the try block, keep the same
validation/assignment logic (models -> validatedModels -> this.models and
this.configPresenter.setProviderModels(this.provider.id, validatedModels)), and
in the catch block log the error via logger.error and rethrow only when
options?.suppressErrors is false to preserve the original behavior.

---

Nitpick comments:
In `@test/main/presenter/llmProviderPresenter/anthropicProvider.test.ts`:
- Around line 171-228: Add an afterEach cleanup to restore the stubbed global
fetch between tests: the tests currently call vi.stubGlobal('fetch') (see
occurrences in anthropicProvider.test.ts around the AiSdkProvider/createProvider
tests) and never restore it, so add an afterEach that calls vi.restoreAllMocks()
and removes or resets global.fetch so subsequent tests don't leak the stubbed
fetch; ensure the cleanup runs in this test file so both the successful fetch
(fetchModels) and failing fetch (refreshModels) tests are isolated.
🪄 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: 9fed5655-1d89-4406-8982-5322a1c1c8d8

📥 Commits

Reviewing files that changed from the base of the PR and between c2a01e1 and fa36258.

📒 Files selected for processing (6)
  • src/main/presenter/llmProviderPresenter/baseProvider.ts
  • src/main/presenter/llmProviderPresenter/providers/aiSdkProvider.ts
  • src/renderer/settings/components/ProviderApiConfig.vue
  • test/main/presenter/llmProviderPresenter/anthropicProvider.test.ts
  • test/main/presenter/llmProviderPresenter/geminiProvider.test.ts
  • test/renderer/components/ProviderApiConfig.test.ts

Comment on lines +894 to +895
const { signal, dispose } = this.createModelRequestSignal(null)

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

Remote model fetches are missing timeout protection.

Both new fetch paths pass null into createModelRequestSignal, so requests can hang indefinitely under network issues.

⏱️ Suggested fix
-    const { signal, dispose } = this.createModelRequestSignal(null)
+    const { signal, dispose } = this.createModelRequestSignal({
+      timeout: this.getModelFetchTimeout()
+    })

Apply in both methods:

  • fetchAnthropicModelsWithFallback
  • fetchGeminiModelsWithFallback

Also applies to: 959-961

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

In `@src/main/presenter/llmProviderPresenter/providers/aiSdkProvider.ts` around
lines 894 - 895, Both fetchAnthropicModelsWithFallback and
fetchGeminiModelsWithFallback call this.createModelRequestSignal(null) which
leaves fetches unbounded; change both calls to pass a sensible timeout (e.g., a
configured ms value or a constant) instead of null, ensure the returned signal
is wired into the fetch request options and that dispose() is still called in
finally to cancel on timeout; update other identical call sites (the second
occurrence around the other fetch path) to match and use the same timeout
constant or config value so remote model fetches are protected from hanging.

Comment on lines +328 to +354
const extractRefreshErrorMessage = (error: unknown): string | null => {
const rawMessage = error instanceof Error ? error.message : String(error)
const normalizedMessage = rawMessage.trim()

if (!normalizedMessage) {
return null
}

try {
const parsed = JSON.parse(normalizedMessage) as {
error?: { message?: string }
message?: string
}

if (typeof parsed.error?.message === 'string' && parsed.error.message.trim()) {
return parsed.error.message.trim()
}

if (typeof parsed.message === 'string' && parsed.message.trim()) {
return parsed.message.trim()
}
} catch {
// ignore JSON parse errors and fall back to the original message
}

return normalizedMessage
}

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

Handle plain-object rejections to avoid [object Object] in toasts.

If IPC rejects with a non-Error object (e.g. { message: '...' }), current logic drops the message. Parse object-shaped errors before String(error).

🩹 Suggested hardening
 const extractRefreshErrorMessage = (error: unknown): string | null => {
-  const rawMessage = error instanceof Error ? error.message : String(error)
+  const objectMessage =
+    error &&
+    typeof error === 'object' &&
+    'message' in error &&
+    typeof (error as { message?: unknown }).message === 'string'
+      ? (error as { message: string }).message
+      : null
+
+  const rawMessage = error instanceof Error ? error.message : objectMessage ?? String(error)
   const normalizedMessage = rawMessage.trim()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const extractRefreshErrorMessage = (error: unknown): string | null => {
const rawMessage = error instanceof Error ? error.message : String(error)
const normalizedMessage = rawMessage.trim()
if (!normalizedMessage) {
return null
}
try {
const parsed = JSON.parse(normalizedMessage) as {
error?: { message?: string }
message?: string
}
if (typeof parsed.error?.message === 'string' && parsed.error.message.trim()) {
return parsed.error.message.trim()
}
if (typeof parsed.message === 'string' && parsed.message.trim()) {
return parsed.message.trim()
}
} catch {
// ignore JSON parse errors and fall back to the original message
}
return normalizedMessage
}
const extractRefreshErrorMessage = (error: unknown): string | null => {
const objectMessage =
error &&
typeof error === 'object' &&
'message' in error &&
typeof (error as { message?: unknown }).message === 'string'
? (error as { message: string }).message
: null
const rawMessage = error instanceof Error ? error.message : objectMessage ?? String(error)
const normalizedMessage = rawMessage.trim()
if (!normalizedMessage) {
return null
}
try {
const parsed = JSON.parse(normalizedMessage) as {
error?: { message?: string }
message?: string
}
if (typeof parsed.error?.message === 'string' && parsed.error.message.trim()) {
return parsed.error.message.trim()
}
if (typeof parsed.message === 'string' && parsed.message.trim()) {
return parsed.message.trim()
}
} catch {
// ignore JSON parse errors and fall back to the original message
}
return normalizedMessage
}
🤖 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 328 -
354, extractRefreshErrorMessage currently converts non-Error rejections to
String(error) which yields “[object Object]” and loses message fields; update
the function (extractRefreshErrorMessage) to first detect plain-object
rejections (e.g., typeof error === 'object' && error !== null) and extract a
string from error.message or error.error?.message when present (trim and
validate), then fall back to the Error instance message or String(error) only if
no message field exists; keep the existing JSON.parse attempt for stringified
objects and preserve the final null/normalizedMessage behavior.

@zhangmo8 zhangmo8 changed the title fix(llm-provider): add model fetching with fallback for Anthropic an… fix(model-list): add model fetching with fallback for Anthropic an… Apr 29, 2026
@zhangmo8 zhangmo8 merged commit 7947222 into dev Apr 29, 2026
3 checks passed
@zhangmo8 zhangmo8 deleted the feature/llm-provider-updates branch April 30, 2026 05:54
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.

1 participant