feat: add CherryIN API host selection settings (#11797)

feat(provider): add CherryIN settings and migration support

- Introduced CherryINSettings component for configuring CherryIN API hosts.
- Updated ProviderSetting to conditionally render CherryINSettings based on provider ID and user language.
- Enhanced providerToAiSdkConfig to include CherryIN API host URLs.
- Incremented store version to 183 and added migration logic to set default CherryIN API hosts.
This commit is contained in:
亢奋猫 2025-12-11 11:19:28 +08:00 committed by GitHub
parent 96085707ce
commit 76524d68c6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 123 additions and 17 deletions

View File

@ -9,6 +9,7 @@ import {
} from '@renderer/hooks/useAwsBedrock' } from '@renderer/hooks/useAwsBedrock'
import { createVertexProvider, isVertexAIConfigured } from '@renderer/hooks/useVertexAI' import { createVertexProvider, isVertexAIConfigured } from '@renderer/hooks/useVertexAI'
import { getProviderByModel } from '@renderer/services/AssistantService' import { getProviderByModel } from '@renderer/services/AssistantService'
import { getProviderById } from '@renderer/services/ProviderService'
import store from '@renderer/store' import store from '@renderer/store'
import { isSystemProvider, type Model, type Provider, SystemProviderIds } from '@renderer/types' import { isSystemProvider, type Model, type Provider, SystemProviderIds } from '@renderer/types'
import type { OpenAICompletionsStreamOptions } from '@renderer/types/aiCoreTypes' import type { OpenAICompletionsStreamOptions } from '@renderer/types/aiCoreTypes'
@ -250,6 +251,12 @@ export function providerToAiSdkConfig(actualProvider: Provider, model: Model): A
if (model.endpoint_type) { if (model.endpoint_type) {
extraOptions.endpointType = model.endpoint_type extraOptions.endpointType = model.endpoint_type
} }
// CherryIN API Host
const cherryinProvider = getProviderById(SystemProviderIds.cherryin)
if (cherryinProvider) {
extraOptions.anthropicBaseURL = cherryinProvider.anthropicApiHost
extraOptions.geminiBaseURL = cherryinProvider.apiHost + '/gemini/v1beta'
}
} }
if (hasProviderConfig(aiSdkProviderId) && aiSdkProviderId !== 'openai-compatible') { if (hasProviderConfig(aiSdkProviderId) && aiSdkProviderId !== 'openai-compatible') {

View File

@ -0,0 +1,74 @@
import { useProvider } from '@renderer/hooks/useProvider'
import { Select } from 'antd'
import type { FC } from 'react'
import { useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
interface CherryINSettingsProps {
providerId: string
apiHost: string
setApiHost: (host: string) => void
}
const API_HOST_OPTIONS = [
{
value: 'https://open.cherryin.cc',
labelKey: '加速域名',
description: 'open.cherryin.cc'
},
{
value: 'https://open.cherryin.net',
labelKey: '国际域名',
description: 'open.cherryin.net'
},
{
value: 'https://open.cherryin.ai',
labelKey: '备用域名',
description: 'open.cherryin.ai'
}
]
const CherryINSettings: FC<CherryINSettingsProps> = ({ providerId, apiHost, setApiHost }) => {
const { updateProvider } = useProvider(providerId)
const { t } = useTranslation()
const getCurrentHost = useMemo(() => {
const matchedOption = API_HOST_OPTIONS.find((option) => apiHost?.includes(option.value.replace('https://', '')))
return matchedOption?.value ?? API_HOST_OPTIONS[0].value
}, [apiHost])
const handleHostChange = useCallback(
(value: string) => {
setApiHost(value)
updateProvider({ apiHost: value, anthropicApiHost: value })
},
[setApiHost, updateProvider]
)
const options = useMemo(
() =>
API_HOST_OPTIONS.map((option) => ({
value: option.value,
label: (
<div className="flex flex-col gap-0.5">
<span>{t(option.labelKey)}</span>
<span className="text-[var(--color-text-3)] text-xs">{t(option.description)}</span>
</div>
)
})),
[t]
)
return (
<Select
value={getCurrentHost}
onChange={handleHostChange}
options={options}
style={{ width: '100%', marginTop: 5 }}
optionLabelProp="label"
labelRender={(option) => option.value}
/>
)
}
export default CherryINSettings

View File

@ -10,7 +10,6 @@ import { PROVIDER_URLS } from '@renderer/config/providers'
import { useTheme } from '@renderer/context/ThemeProvider' import { useTheme } from '@renderer/context/ThemeProvider'
import { useAllProviders, useProvider, useProviders } from '@renderer/hooks/useProvider' import { useAllProviders, useProvider, useProviders } from '@renderer/hooks/useProvider'
import { useTimer } from '@renderer/hooks/useTimer' import { useTimer } from '@renderer/hooks/useTimer'
import i18n from '@renderer/i18n'
import AnthropicSettings from '@renderer/pages/settings/ProviderSettings/AnthropicSettings' import AnthropicSettings from '@renderer/pages/settings/ProviderSettings/AnthropicSettings'
import { ModelList } from '@renderer/pages/settings/ProviderSettings/ModelList' import { ModelList } from '@renderer/pages/settings/ProviderSettings/ModelList'
import { checkApi } from '@renderer/services/ApiService' import { checkApi } from '@renderer/services/ApiService'
@ -53,6 +52,7 @@ import {
} from '..' } from '..'
import ApiOptionsSettingsPopup from './ApiOptionsSettings/ApiOptionsSettingsPopup' import ApiOptionsSettingsPopup from './ApiOptionsSettings/ApiOptionsSettingsPopup'
import AwsBedrockSettings from './AwsBedrockSettings' import AwsBedrockSettings from './AwsBedrockSettings'
import CherryINSettings from './CherryINSettings'
import CustomHeaderPopup from './CustomHeaderPopup' import CustomHeaderPopup from './CustomHeaderPopup'
import DMXAPISettings from './DMXAPISettings' import DMXAPISettings from './DMXAPISettings'
import GithubCopilotSettings from './GithubCopilotSettings' import GithubCopilotSettings from './GithubCopilotSettings'
@ -99,13 +99,15 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
const [anthropicApiHost, setAnthropicHost] = useState<string | undefined>(provider.anthropicApiHost) const [anthropicApiHost, setAnthropicHost] = useState<string | undefined>(provider.anthropicApiHost)
const [apiVersion, setApiVersion] = useState(provider.apiVersion) const [apiVersion, setApiVersion] = useState(provider.apiVersion)
const [activeHostField, setActiveHostField] = useState<HostField>('apiHost') const [activeHostField, setActiveHostField] = useState<HostField>('apiHost')
const { t } = useTranslation() const { t, i18n } = useTranslation()
const { theme } = useTheme() const { theme } = useTheme()
const { setTimeoutTimer } = useTimer() const { setTimeoutTimer } = useTimer()
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const isAzureOpenAI = isAzureOpenAIProvider(provider) const isAzureOpenAI = isAzureOpenAIProvider(provider)
const isDmxapi = provider.id === 'dmxapi' const isDmxapi = provider.id === 'dmxapi'
const isCherryIN = provider.id === 'cherryin'
const isChineseUser = i18n.language.startsWith('zh')
const noAPIInputProviders = ['aws-bedrock'] as const satisfies SystemProviderId[] const noAPIInputProviders = ['aws-bedrock'] as const satisfies SystemProviderId[]
const hideApiInput = noAPIInputProviders.some((id) => id === provider.id) const hideApiInput = noAPIInputProviders.some((id) => id === provider.id)
const noAPIKeyInputProviders = ['copilot', 'vertexai'] as const satisfies SystemProviderId[] const noAPIKeyInputProviders = ['copilot', 'vertexai'] as const satisfies SystemProviderId[]
@ -338,13 +340,16 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
}, [provider.anthropicApiHost]) }, [provider.anthropicApiHost])
const canConfigureAnthropicHost = useMemo(() => { const canConfigureAnthropicHost = useMemo(() => {
if (isCherryIN) {
return false
}
if (isNewApiProvider(provider)) { if (isNewApiProvider(provider)) {
return true return true
} }
return ( return (
provider.type !== 'anthropic' && isSystemProviderId(provider.id) && isAnthropicCompatibleProviderId(provider.id) provider.type !== 'anthropic' && isSystemProviderId(provider.id) && isAnthropicCompatibleProviderId(provider.id)
) )
}, [provider]) }, [isCherryIN, provider])
const anthropicHostPreview = useMemo(() => { const anthropicHostPreview = useMemo(() => {
const rawHost = anthropicApiHost ?? provider.anthropicApiHost const rawHost = anthropicApiHost ?? provider.anthropicApiHost
@ -513,19 +518,23 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
</SettingSubtitle> </SettingSubtitle>
{activeHostField === 'apiHost' && ( {activeHostField === 'apiHost' && (
<> <>
<Space.Compact style={{ width: '100%', marginTop: 5 }}> {isCherryIN && isChineseUser ? (
<Input <CherryINSettings providerId={provider.id} apiHost={apiHost} setApiHost={setApiHost} />
value={apiHost} ) : (
placeholder={t('settings.provider.api_host')} <Space.Compact style={{ width: '100%', marginTop: 5 }}>
onChange={(e) => setApiHost(e.target.value)} <Input
onBlur={onUpdateApiHost} value={apiHost}
/> placeholder={t('settings.provider.api_host')}
{isApiHostResettable && ( onChange={(e) => setApiHost(e.target.value)}
<Button danger onClick={onReset}> onBlur={onUpdateApiHost}
{t('settings.provider.api.url.reset')} />
</Button> {isApiHostResettable && (
)} <Button danger onClick={onReset}>
</Space.Compact> {t('settings.provider.api.url.reset')}
</Button>
)}
</Space.Compact>
)}
{isVertexProvider(provider) && ( {isVertexProvider(provider) && (
<SettingHelpTextRow> <SettingHelpTextRow>
<SettingHelpText>{t('settings.provider.vertex_ai.api_host_help')}</SettingHelpText> <SettingHelpText>{t('settings.provider.vertex_ai.api_host_help')}</SettingHelpText>

View File

@ -67,7 +67,7 @@ const persistedReducer = persistReducer(
{ {
key: 'cherry-studio', key: 'cherry-studio',
storage, storage,
version: 182, version: 183,
blacklist: ['runtime', 'messages', 'messageBlocks', 'tabs', 'toolPermissions'], blacklist: ['runtime', 'messages', 'messageBlocks', 'tabs', 'toolPermissions'],
migrate migrate
}, },

View File

@ -2976,6 +2976,22 @@ const migrateConfig = {
logger.error('migrate 182 error', error as Error) logger.error('migrate 182 error', error as Error)
return state return state
} }
},
'183': (state: RootState) => {
try {
state.llm.providers.forEach((provider) => {
if (provider.id === SystemProviderIds.cherryin) {
provider.apiHost = 'https://open.cherryin.cc'
provider.anthropicApiHost = 'https://open.cherryin.cc'
}
})
state.llm.providers = moveProvider(state.llm.providers, SystemProviderIds.poe, 10)
logger.info('migrate 183 success')
return state
} catch (error) {
logger.error('migrate 183 error', error as Error)
return state
}
} }
} }