diff --git a/src/renderer/components/ArtifactPreview/office/__tests__/WordPreviewPanel.test.tsx b/src/renderer/components/ArtifactPreview/office/__tests__/WordPreviewPanel.test.tsx index 55a35cddc68..39af2448cf4 100644 --- a/src/renderer/components/ArtifactPreview/office/__tests__/WordPreviewPanel.test.tsx +++ b/src/renderer/components/ArtifactPreview/office/__tests__/WordPreviewPanel.test.tsx @@ -211,6 +211,7 @@ describe('WordPreviewPanel', () => { render() await waitFor(() => expect(screen.getByTestId('docx-preview-page-indicator')).toHaveTextContent('1 / 2')) + await waitFor(() => expect(mocks.intersectionObserverInstances.at(-1)).toBeDefined()) const observer = mocks.intersectionObserverInstances.at(-1) expect(observer).toBeDefined() diff --git a/src/renderer/hooks/agent/__tests__/useAgentModelFilter.test.ts b/src/renderer/hooks/agent/__tests__/useAgentModelFilter.test.ts index 7714aec549d..7b11724cc0f 100644 --- a/src/renderer/hooks/agent/__tests__/useAgentModelFilter.test.ts +++ b/src/renderer/hooks/agent/__tests__/useAgentModelFilter.test.ts @@ -43,6 +43,12 @@ describe('useAgentModelFilter', () => { id: 'vertex', defaultChatEndpoint: 'google-generate-content', authType: 'iam-gcp' + }, + { + id: 'cherryai', + presetProviderId: 'cherryai', + defaultChatEndpoint: 'openai-chat-completions', + authType: 'api-key' } ] }) @@ -63,6 +69,14 @@ describe('useAgentModelFilter', () => { expect(result.current({ ...model(), providerId: 'google-custom', id: 'google-custom::gemini-2.5-pro' })).toBe(false) }) + it('filters the managed CherryAI default model for Claude Code agents', () => { + const { result } = renderHook(() => useAgentModelFilter('claude-code')) + + expect( + result.current({ ...model(), providerId: 'cherryai', id: 'cherryai::qwen', apiModelId: 'qwen', name: 'Qwen' }) + ).toBe(false) + }) + it('marks its predicate as an agent picker so selectors surface agent-only providers', () => { const { result } = renderHook(() => useAgentModelFilter('claude-code')) diff --git a/src/renderer/hooks/agent/useAgentModelFilter.ts b/src/renderer/hooks/agent/useAgentModelFilter.ts index 4df7ac7ee25..bab1597b85e 100644 --- a/src/renderer/hooks/agent/useAgentModelFilter.ts +++ b/src/renderer/hooks/agent/useAgentModelFilter.ts @@ -13,8 +13,7 @@ import { useProviders } from '@renderer/hooks/useProvider' import type { AgentType } from '@shared/data/types/agent' import type { Model } from '@shared/data/types/model' -import { isNonChatModel } from '@shared/utils/model' -import { isGeminiProvider } from '@shared/utils/provider' +import { isAgentRuntimeSupportedModel, isNonChatModel } from '@shared/utils/model' import { useMemo } from 'react' const baseAgentFilter = (model: Model): boolean => !isNonChatModel(model) @@ -40,25 +39,17 @@ export function modelFilterIncludesAgentOnlyProviders(filter?: (model: Model) => export function useAgentModelFilter(agentType: AgentType | undefined): (model: Model) => boolean { const { providers } = useProviders() - const geminiProviderIds = useMemo(() => { - const ids = new Set() - for (const provider of providers) { - if (isGeminiProvider(provider)) { - ids.add(provider.id) - } - } - return ids - }, [providers]) + const providersById = useMemo(() => new Map(providers.map((provider) => [provider.id, provider])), [providers]) return useMemo(() => { const predicate: AgentModelFilter = (model: Model) => { if (!baseAgentFilter(model)) return false if (agentType === 'claude-code') { - return !geminiProviderIds.has(model.providerId) + return isAgentRuntimeSupportedModel(model, providersById.get(model.providerId)) } return true } predicate[AGENT_ONLY_FILTER] = true return predicate - }, [agentType, geminiProviderIds]) + }, [agentType, providersById]) } diff --git a/src/renderer/i18n/locales/en-us.json b/src/renderer/i18n/locales/en-us.json index 735d137482c..878a5b1138b 100644 --- a/src/renderer/i18n/locales/en-us.json +++ b/src/renderer/i18n/locales/en-us.json @@ -8,7 +8,7 @@ }, "model": { "supported_providers": "Supported Providers", - "tooltip": "Currently, only models that support Anthropic endpoints are available for the Agent feature.", + "tooltip": "Most chat models are available for Agents. Gemini providers are not supported yet.", "view_providers": "View supported providers" }, "title": "Add Agent", diff --git a/src/renderer/i18n/locales/zh-cn.json b/src/renderer/i18n/locales/zh-cn.json index 1175bc96d61..df36879b7de 100644 --- a/src/renderer/i18n/locales/zh-cn.json +++ b/src/renderer/i18n/locales/zh-cn.json @@ -8,7 +8,7 @@ }, "model": { "supported_providers": "支持的服务商", - "tooltip": "目前,只有支持 Anthropic 端点的模型可用于智能体功能。", + "tooltip": "目前大多数聊天模型可用于智能体,暂不支持 Gemini 服务商。", "view_providers": "查看支持的服务商" }, "title": "添加智能体", diff --git a/src/renderer/i18n/locales/zh-tw.json b/src/renderer/i18n/locales/zh-tw.json index dce9ffc6cb5..b19819ab544 100644 --- a/src/renderer/i18n/locales/zh-tw.json +++ b/src/renderer/i18n/locales/zh-tw.json @@ -8,7 +8,7 @@ }, "model": { "supported_providers": "支援的服務商", - "tooltip": "目前,僅支援 Anthropic 端點的模型可供 Agent 功能使用。", + "tooltip": "目前大多數聊天模型可供 Agent 使用,暫不支援 Gemini 服務商。", "view_providers": "檢視支援的服務商" }, "title": "新增 Agent", diff --git a/src/renderer/pages/settings/ProviderSettings/ProviderList/ProviderList.tsx b/src/renderer/pages/settings/ProviderSettings/ProviderList/ProviderList.tsx index 31733f0b7d4..fb82f9a8c13 100644 --- a/src/renderer/pages/settings/ProviderSettings/ProviderList/ProviderList.tsx +++ b/src/renderer/pages/settings/ProviderSettings/ProviderList/ProviderList.tsx @@ -8,7 +8,7 @@ import { matchKeywordsInProvider } from '@renderer/pages/settings/ProviderSettings/utils/providerDisplay' import type { Provider } from '@shared/data/types/provider' -import { canManageProvider, isAnthropicSupportedProvider } from '@shared/utils/provider' +import { canManageProvider, isAgentSupportedProvider } from '@shared/utils/provider' import { Plus } from 'lucide-react' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -112,7 +112,7 @@ export default function ProviderList({ selectedProviderId, filterModeHint, onSel if (filterMode === 'disabled' && provider.isEnabled) { return false } - if (filterMode === 'agent' && !isAnthropicSupportedProvider(provider)) { + if (filterMode === 'agent' && !isAgentSupportedProvider(provider)) { return false } const keywords = searchText.toLowerCase().split(/\s+/).filter(Boolean) diff --git a/src/renderer/pages/settings/ProviderSettings/ProviderList/providerFilterMode.ts b/src/renderer/pages/settings/ProviderSettings/ProviderList/providerFilterMode.ts index 7a4568a931b..d8524384a09 100644 --- a/src/renderer/pages/settings/ProviderSettings/ProviderList/providerFilterMode.ts +++ b/src/renderer/pages/settings/ProviderSettings/ProviderList/providerFilterMode.ts @@ -5,7 +5,7 @@ * - `enabled`: only `isEnabled === true` * - `disabled`: only `isEnabled === false` * - `all` (default): every provider - * - `agent`: only providers that speak the Anthropic protocol (orthogonal to - * the enabled/disabled axis) + * - `agent`: only providers supported by the agent runtime (orthogonal to the + * enabled/disabled axis) */ export type ProviderFilterMode = 'enabled' | 'disabled' | 'all' | 'agent' diff --git a/src/renderer/pages/settings/ProviderSettings/__tests__/ProviderList.test.tsx b/src/renderer/pages/settings/ProviderSettings/__tests__/ProviderList.test.tsx index 3aa746c99dd..53f83e0bc49 100644 --- a/src/renderer/pages/settings/ProviderSettings/__tests__/ProviderList.test.tsx +++ b/src/renderer/pages/settings/ProviderSettings/__tests__/ProviderList.test.tsx @@ -314,17 +314,33 @@ describe('ProviderList', () => { }) }) - it('applies an external filter hint without making the page own list filter state', () => { + it('applies the agent filter hint without hiding gateway-routable providers', () => { const onSelectProvider = vi.fn() + useProvidersMock.mockReturnValue({ + providers: [ + ...providers, + { + id: 'gemini', + name: 'Gemini', + presetProviderId: 'gemini', + defaultChatEndpoint: ENDPOINT_TYPE.GOOGLE_GENERATE_CONTENT, + authType: 'api-key', + isEnabled: true + } + ], + createProvider: vi.fn() + }) const { rerender } = render() expect(screen.getByText('OpenAI')).toBeInTheDocument() expect(screen.getByText('Anthropic')).toBeInTheDocument() + expect(screen.getByText('Gemini')).toBeInTheDocument() rerender() - expect(screen.queryByText('OpenAI')).not.toBeInTheDocument() + expect(screen.getByText('OpenAI')).toBeInTheDocument() expect(screen.getByText('Anthropic')).toBeInTheDocument() + expect(screen.queryByText('Gemini')).not.toBeInTheDocument() const filterButton = screen.getByRole('button', { name: '筛选服务商' }) expect(filterButton).not.toHaveClass('bg-primary/10') expect(filterButton.querySelector('svg')).toHaveClass('text-primary!') diff --git a/src/shared/utils/model.ts b/src/shared/utils/model.ts index c972dddee47..20cfd93ec19 100644 --- a/src/shared/utils/model.ts +++ b/src/shared/utils/model.ts @@ -12,9 +12,12 @@ */ import { MODALITY, VENDOR_PATTERNS } from '@cherrystudio/provider-registry' -import { CHERRYAI_PROVIDER_ID } from '@shared/data/presets/cherryai' +import { CHERRYAI_PROVIDER_ID, isManagedCherryAiDefaultModel } from '@shared/data/presets/cherryai' import type { Model, RuntimeReasoning, ThinkingTokenLimits } from '@shared/data/types/model' import { MODEL_CAPABILITY, parseUniqueModelId } from '@shared/data/types/model' +import type { Provider } from '@shared/data/types/provider' + +import { isAgentSupportedProvider } from './provider' /** Check if model has reasoning capability */ export const isReasoningModel = (model: Model): boolean => @@ -94,6 +97,12 @@ export const isNonChatModel = (model: Model): boolean => isTextToSpeechModel(model) || isSpeechToTextModel(model) +export const isAgentRuntimeSupportedModel = (model: Model, provider?: Provider): boolean => { + if (isNonChatModel(model)) return false + if (provider && !isAgentSupportedProvider(provider)) return false + return !isManagedCherryAiDefaultModel(model.providerId, getRawModelId(model)) +} + // --------------------------------------------------------------------------- // Reasoning configuration // --------------------------------------------------------------------------- diff --git a/src/shared/utils/provider.ts b/src/shared/utils/provider.ts index 30d04bdc822..b62415b9768 100644 --- a/src/shared/utils/provider.ts +++ b/src/shared/utils/provider.ts @@ -145,6 +145,10 @@ export function isAnthropicSupportedProvider(provider: Provider): boolean { return getProviderHostTopology(provider).hasAnthropicEndpoint } +export function isAgentSupportedProvider(provider: Provider): boolean { + return !isGeminiProvider(provider) +} + export function isSupportUrlContextProvider(provider: Provider): boolean { return ( isGeminiProvider(provider) ||