Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions src/renderer/hooks/agent/__tests__/useAgentModelFilter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}
]
})
Expand All @@ -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'))

Expand Down
17 changes: 4 additions & 13 deletions src/renderer/hooks/agent/useAgentModelFilter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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<string>()
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<AgentModelFilter>(() => {
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])
}
2 changes: 1 addition & 1 deletion src/renderer/i18n/locales/en-us.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/i18n/locales/zh-cn.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
},
"model": {
"supported_providers": "支持的服务商",
"tooltip": "目前,只有支持 Anthropic 端点的模型可用于智能体功能。",
"tooltip": "目前大多数聊天模型可用于智能体,暂不支持 Gemini 服务商。",
"view_providers": "查看支持的服务商"
},
"title": "添加智能体",
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/i18n/locales/zh-tw.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
},
"model": {
"supported_providers": "支援的服務商",
"tooltip": "目前,僅支援 Anthropic 端點的模型可供 Agent 功能使用。",
"tooltip": "目前大多數聊天模型可供 Agent 使用,暫不支援 Gemini 服務商。",
"view_providers": "檢視支援的服務商"
},
"title": "新增 Agent",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Original file line number Diff line number Diff line change
Expand Up @@ -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(<ProviderList selectedProviderId="openai" onSelectProvider={onSelectProvider} />)

expect(screen.getByText('OpenAI')).toBeInTheDocument()
expect(screen.getByText('Anthropic')).toBeInTheDocument()
expect(screen.getByText('Gemini')).toBeInTheDocument()

rerender(<ProviderList selectedProviderId="openai" filterModeHint="agent" onSelectProvider={onSelectProvider} />)

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!')
Expand Down
11 changes: 10 additions & 1 deletion src/shared/utils/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 =>
Expand Down Expand Up @@ -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
// ---------------------------------------------------------------------------
Expand Down
4 changes: 4 additions & 0 deletions src/shared/utils/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) ||
Expand Down
Loading