mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-28 13:31:32 +08:00
Add Anthropic API Host support for compatible providers
- Add `anthropicApiHost` field to Provider type - Update provider config and migration to set Anthropic endpoints - Add UI for configuring Anthropic API Host in provider settings - Update SDK client logic to use Anthropic API Host when available - Add i18n strings for Anthropic API Host configuration
This commit is contained in:
parent
ae9e12b276
commit
35b885798b
@ -70,10 +70,15 @@ export function getSdkClient(provider: Provider, oauthToken?: string | null): An
|
||||
}
|
||||
})
|
||||
}
|
||||
const baseURL =
|
||||
provider.type === 'anthropic'
|
||||
? provider.apiHost
|
||||
: (provider.anthropicApiHost && provider.anthropicApiHost.trim()) || provider.apiHost
|
||||
|
||||
return new Anthropic({
|
||||
apiKey: provider.apiKey,
|
||||
authToken: provider.apiKey,
|
||||
baseURL: provider.apiHost,
|
||||
baseURL,
|
||||
dangerouslyAllowBrowser: true,
|
||||
defaultHeaders: {
|
||||
'anthropic-beta': 'output-128k-2025-02-19',
|
||||
|
||||
@ -79,9 +79,37 @@ function handleSpecialProviders(model: Model, provider: Provider): Provider {
|
||||
/**
|
||||
* 格式化provider的API Host
|
||||
*/
|
||||
function formatAnthropicApiHost(host: string): string {
|
||||
const trimmedHost = host?.trim()
|
||||
|
||||
if (!trimmedHost) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (trimmedHost.endsWith('/')) {
|
||||
return trimmedHost
|
||||
}
|
||||
|
||||
if (trimmedHost.endsWith('/v1')) {
|
||||
return `${trimmedHost}/`
|
||||
}
|
||||
|
||||
return formatApiHost(trimmedHost)
|
||||
}
|
||||
|
||||
function formatProviderApiHost(provider: Provider): Provider {
|
||||
const formatted = { ...provider }
|
||||
if (formatted.type === 'gemini') {
|
||||
if (formatted.anthropicApiHost) {
|
||||
formatted.anthropicApiHost = formatAnthropicApiHost(formatted.anthropicApiHost)
|
||||
}
|
||||
|
||||
if (formatted.type === 'anthropic') {
|
||||
const baseHost = formatted.anthropicApiHost || formatted.apiHost
|
||||
formatted.apiHost = formatAnthropicApiHost(baseHost)
|
||||
if (!formatted.anthropicApiHost) {
|
||||
formatted.anthropicApiHost = formatted.apiHost
|
||||
}
|
||||
} else if (formatted.type === 'gemini') {
|
||||
formatted.apiHost = formatApiHost(formatted.apiHost, 'v1beta')
|
||||
} else {
|
||||
formatted.apiHost = formatApiHost(formatted.apiHost)
|
||||
|
||||
@ -104,6 +104,7 @@ export const SYSTEM_PROVIDERS_CONFIG: Record<SystemProviderId, SystemProvider> =
|
||||
type: 'openai',
|
||||
apiKey: '',
|
||||
apiHost: 'https://aihubmix.com',
|
||||
anthropicApiHost: 'https://aihubmix.com/anthropic',
|
||||
models: SYSTEM_MODELS.aihubmix,
|
||||
isSystem: true,
|
||||
enabled: false
|
||||
@ -124,6 +125,7 @@ export const SYSTEM_PROVIDERS_CONFIG: Record<SystemProviderId, SystemProvider> =
|
||||
type: 'openai',
|
||||
apiKey: '',
|
||||
apiHost: 'https://open.bigmodel.cn/api/paas/v4/',
|
||||
anthropicApiHost: 'https://open.bigmodel.cn/api/anthropic',
|
||||
models: SYSTEM_MODELS.zhipu,
|
||||
isSystem: true,
|
||||
enabled: false
|
||||
@ -134,6 +136,7 @@ export const SYSTEM_PROVIDERS_CONFIG: Record<SystemProviderId, SystemProvider> =
|
||||
type: 'openai',
|
||||
apiKey: '',
|
||||
apiHost: 'https://api.deepseek.com',
|
||||
anthropicApiHost: 'https://api.deepseek.com/anthropic',
|
||||
models: SYSTEM_MODELS.deepseek,
|
||||
isSystem: true,
|
||||
enabled: false
|
||||
@ -379,6 +382,7 @@ export const SYSTEM_PROVIDERS_CONFIG: Record<SystemProviderId, SystemProvider> =
|
||||
type: 'openai',
|
||||
apiKey: '',
|
||||
apiHost: 'https://api.moonshot.cn',
|
||||
anthropicApiHost: 'https://api.moonshot.cn/anthropic',
|
||||
models: SYSTEM_MODELS.moonshot,
|
||||
isSystem: true,
|
||||
enabled: false
|
||||
@ -399,6 +403,7 @@ export const SYSTEM_PROVIDERS_CONFIG: Record<SystemProviderId, SystemProvider> =
|
||||
type: 'openai',
|
||||
apiKey: '',
|
||||
apiHost: 'https://dashscope.aliyuncs.com/compatible-mode/v1/',
|
||||
anthropicApiHost: 'https://dashscope.aliyuncs.com/api/v2/apps/claude-code-proxy',
|
||||
models: SYSTEM_MODELS.dashscope,
|
||||
isSystem: true,
|
||||
enabled: false
|
||||
@ -539,6 +544,7 @@ export const SYSTEM_PROVIDERS_CONFIG: Record<SystemProviderId, SystemProvider> =
|
||||
type: 'openai',
|
||||
apiKey: '',
|
||||
apiHost: 'https://api-inference.modelscope.cn/v1/',
|
||||
anthropicApiHost: 'https://api-inference.modelscope.cn',
|
||||
models: SYSTEM_MODELS.modelscope,
|
||||
isSystem: true,
|
||||
enabled: false
|
||||
|
||||
@ -3995,6 +3995,12 @@
|
||||
}
|
||||
},
|
||||
"api_host": "API Host",
|
||||
"api_host_tooltip": "Override only when your provider requires a custom OpenAI-compatible endpoint.",
|
||||
"api_host_preview": "Preview: {{url}}",
|
||||
"anthropic_api_host": "Anthropic API Host",
|
||||
"anthropic_api_host_tooltip": "Use only when the provider offers a Claude-compatible base URL.",
|
||||
"anthropic_api_host_preview": "Anthropic preview: {{url}}",
|
||||
"anthropic_api_host_tip": "Only configure this when your provider exposes an Anthropic-compatible endpoint. Ending with / ignores v1, ending with # forces use of input address.",
|
||||
"api_key": {
|
||||
"label": "API Key",
|
||||
"tip": "Multiple keys separated by commas or spaces"
|
||||
|
||||
@ -3995,6 +3995,12 @@
|
||||
}
|
||||
},
|
||||
"api_host": "API 地址",
|
||||
"api_host_tooltip": "仅在服务商需要自定义的 OpenAI 兼容地址时覆盖。",
|
||||
"api_host_preview": "预览:{{url}}",
|
||||
"anthropic_api_host": "Anthropic API 地址",
|
||||
"anthropic_api_host_tooltip": "仅当服务商提供 Claude 兼容的基础地址时填写。",
|
||||
"anthropic_api_host_preview": "Anthropic 预览:{{url}}",
|
||||
"anthropic_api_host_tip": "仅在服务商提供兼容 Anthropic 的地址时填写。以 / 结尾会忽略自动追加的 v1,以 # 结尾则强制使用原始地址。",
|
||||
"api_key": {
|
||||
"label": "API 密钥",
|
||||
"tip": "多个密钥使用逗号或空格分隔"
|
||||
|
||||
@ -3995,6 +3995,12 @@
|
||||
}
|
||||
},
|
||||
"api_host": "API 主機地址",
|
||||
"api_host_tooltip": "僅在服務商需要自訂的 OpenAI 相容端點時才覆蓋。",
|
||||
"api_host_preview": "預覽:{{url}}",
|
||||
"anthropic_api_host": "Anthropic API 主機地址",
|
||||
"anthropic_api_host_tooltip": "僅在服務商提供 Claude 相容的基礎網址時設定。",
|
||||
"anthropic_api_host_preview": "Anthropic 預覽:{{url}}",
|
||||
"anthropic_api_host_tip": "僅在服務商提供與 Anthropic 相容的網址時設定。以 / 結尾會忽略自動附加的 v1,以 # 結尾則強制使用原始地址。",
|
||||
"api_key": {
|
||||
"label": "API 金鑰",
|
||||
"tip": "多個金鑰使用逗號或空格分隔"
|
||||
|
||||
@ -55,11 +55,14 @@ interface Props {
|
||||
providerId: string
|
||||
}
|
||||
|
||||
const ANTHROPIC_COMPATIBLE_PROVIDER_IDS = ['deepseek', 'moonshot', 'zhipu', 'dashscope', 'modelscope', 'aihubmix']
|
||||
|
||||
const ProviderSetting: FC<Props> = ({ providerId }) => {
|
||||
const { provider, updateProvider, models } = useProvider(providerId)
|
||||
const allProviders = useAllProviders()
|
||||
const { updateProviders } = useProviders()
|
||||
const [apiHost, setApiHost] = useState(provider.apiHost)
|
||||
const [anthropicApiHost, setAnthropicHost] = useState<string | undefined>(provider.anthropicApiHost)
|
||||
const [apiVersion, setApiVersion] = useState(provider.apiVersion)
|
||||
const { t } = useTranslation()
|
||||
const { theme } = useTheme()
|
||||
@ -140,6 +143,17 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
|
||||
}
|
||||
}
|
||||
|
||||
const onUpdateAnthropicHost = () => {
|
||||
const trimmedHost = anthropicApiHost?.trim()
|
||||
|
||||
if (trimmedHost) {
|
||||
updateProvider({ anthropicApiHost: trimmedHost })
|
||||
setAnthropicHost(trimmedHost)
|
||||
} else {
|
||||
updateProvider({ anthropicApiHost: undefined })
|
||||
setAnthropicHost(undefined)
|
||||
}
|
||||
}
|
||||
const onUpdateApiVersion = () => updateProvider({ apiVersion })
|
||||
|
||||
const openApiKeyList = async () => {
|
||||
@ -245,6 +259,34 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
|
||||
setApiHost(provider.apiHost)
|
||||
}, [provider.apiHost, provider.id])
|
||||
|
||||
useEffect(() => {
|
||||
setAnthropicHost(provider.anthropicApiHost)
|
||||
}, [provider.anthropicApiHost])
|
||||
|
||||
const canConfigureAnthropicHost = useMemo(() => {
|
||||
return provider.type !== 'anthropic' && ANTHROPIC_COMPATIBLE_PROVIDER_IDS.includes(provider.id)
|
||||
}, [provider])
|
||||
|
||||
const anthropicHostPreview = useMemo(() => {
|
||||
const rawHost = (anthropicApiHost ?? provider.anthropicApiHost)?.trim()
|
||||
if (!rawHost) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (/\/messages\/?$/.test(rawHost)) {
|
||||
return rawHost.replace(/\/$/, '')
|
||||
}
|
||||
|
||||
let normalizedHost = rawHost
|
||||
if (/\/v\d+(?:\/)?$/i.test(normalizedHost)) {
|
||||
normalizedHost = normalizedHost.replace(/\/$/, '')
|
||||
} else {
|
||||
normalizedHost = formatApiHost(normalizedHost).replace(/\/$/, '')
|
||||
}
|
||||
|
||||
return `${normalizedHost}/messages`
|
||||
}, [anthropicApiHost, provider.anthropicApiHost])
|
||||
|
||||
const isAnthropicOAuth = () => provider.id === 'anthropic' && provider.authType === 'oauth'
|
||||
|
||||
return (
|
||||
@ -351,7 +393,9 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
|
||||
{!isDmxapi && !isAnthropicOAuth() && (
|
||||
<>
|
||||
<SettingSubtitle style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
{t('settings.provider.api_host')}
|
||||
<Tooltip title={t('settings.provider.api_host_tooltip')} mouseEnterDelay={0.3}>
|
||||
<span>{t('settings.provider.api_host')}</span>
|
||||
</Tooltip>
|
||||
<Button
|
||||
type="text"
|
||||
onClick={() => CustomHeaderPopup.show({ provider })}
|
||||
@ -371,17 +415,53 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
|
||||
</Button>
|
||||
)}
|
||||
</Space.Compact>
|
||||
|
||||
{(isOpenAIProvider(provider) || isAnthropicProvider(provider)) && (
|
||||
<SettingHelpTextRow style={{ justifyContent: 'space-between' }}>
|
||||
<SettingHelpText
|
||||
style={{ marginLeft: 6, marginRight: '1em', whiteSpace: 'break-spaces', wordBreak: 'break-all' }}>
|
||||
{hostPreview()}
|
||||
{t('settings.provider.api_host_preview', { url: hostPreview() })}
|
||||
</SettingHelpText>
|
||||
<SettingHelpText style={{ minWidth: 'fit-content' }}>
|
||||
{t('settings.provider.api.url.tip')}
|
||||
</SettingHelpText>
|
||||
</SettingHelpTextRow>
|
||||
)}
|
||||
|
||||
{canConfigureAnthropicHost && (
|
||||
<>
|
||||
<SettingSubtitle
|
||||
style={{
|
||||
marginTop: 5,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between'
|
||||
}}>
|
||||
<Tooltip title={t('settings.provider.anthropic_api_host_tooltip')} mouseEnterDelay={0.3}>
|
||||
<span>{t('settings.provider.anthropic_api_host')}</span>
|
||||
</Tooltip>
|
||||
</SettingSubtitle>
|
||||
<Space.Compact style={{ width: '100%', marginTop: 5 }}>
|
||||
<Input
|
||||
value={anthropicApiHost ?? ''}
|
||||
placeholder={t('settings.provider.anthropic_api_host')}
|
||||
onChange={(e) => setAnthropicHost(e.target.value)}
|
||||
onBlur={onUpdateAnthropicHost}
|
||||
/>
|
||||
</Space.Compact>
|
||||
<SettingHelpTextRow style={{ justifyContent: 'space-between' }}>
|
||||
<SettingHelpText
|
||||
style={{ marginLeft: 6, marginRight: '1em', whiteSpace: 'break-spaces', wordBreak: 'break-all' }}>
|
||||
{t('settings.provider.anthropic_api_host_preview', {
|
||||
url: anthropicHostPreview || '—'
|
||||
})}
|
||||
</SettingHelpText>
|
||||
<SettingHelpText style={{ minWidth: 'fit-content', whiteSpace: 'normal' }}>
|
||||
{t('settings.provider.anthropic_api_host_tip')}
|
||||
</SettingHelpText>
|
||||
</SettingHelpTextRow>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
|
||||
@ -65,7 +65,7 @@ const persistedReducer = persistReducer(
|
||||
{
|
||||
key: 'cherry-studio',
|
||||
storage,
|
||||
version: 158,
|
||||
version: 159,
|
||||
blacklist: ['runtime', 'messages', 'messageBlocks', 'tabs'],
|
||||
migrate
|
||||
},
|
||||
|
||||
@ -51,9 +51,18 @@ const logger = loggerService.withContext('Migrate')
|
||||
// remove logo base64 data to reduce the size of the state
|
||||
function removeMiniAppIconsFromState(state: RootState) {
|
||||
if (state.minapps) {
|
||||
state.minapps.enabled = state.minapps.enabled.map((app) => ({ ...app, logo: undefined }))
|
||||
state.minapps.disabled = state.minapps.disabled.map((app) => ({ ...app, logo: undefined }))
|
||||
state.minapps.pinned = state.minapps.pinned.map((app) => ({ ...app, logo: undefined }))
|
||||
state.minapps.enabled = state.minapps.enabled.map((app) => ({
|
||||
...app,
|
||||
logo: undefined
|
||||
}))
|
||||
state.minapps.disabled = state.minapps.disabled.map((app) => ({
|
||||
...app,
|
||||
logo: undefined
|
||||
}))
|
||||
state.minapps.pinned = state.minapps.pinned.map((app) => ({
|
||||
...app,
|
||||
logo: undefined
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
@ -96,7 +105,10 @@ function updateProvider(state: RootState, id: string, provider: Partial<Provider
|
||||
if (state.llm.providers) {
|
||||
const index = state.llm.providers.findIndex((p) => p.id === id)
|
||||
if (index !== -1) {
|
||||
state.llm.providers[index] = { ...state.llm.providers[index], ...provider }
|
||||
state.llm.providers[index] = {
|
||||
...state.llm.providers[index],
|
||||
...provider
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -544,7 +556,10 @@ const migrateConfig = {
|
||||
...state.llm,
|
||||
providers: state.llm.providers.map((provider) => {
|
||||
if (provider.id === 'azure-openai') {
|
||||
provider.models = provider.models.map((model) => ({ ...model, provider: 'azure-openai' }))
|
||||
provider.models = provider.models.map((model) => ({
|
||||
...model,
|
||||
provider: 'azure-openai'
|
||||
}))
|
||||
}
|
||||
return provider
|
||||
})
|
||||
@ -600,7 +615,10 @@ const migrateConfig = {
|
||||
runAsyncFunction(async () => {
|
||||
const _topic = await db.topics.get(topic.id)
|
||||
if (_topic) {
|
||||
const messages = (_topic?.messages || []).map((message) => ({ ...message, assistantId: assistant.id }))
|
||||
const messages = (_topic?.messages || []).map((message) => ({
|
||||
...message,
|
||||
assistantId: assistant.id
|
||||
}))
|
||||
db.topics.put({ ..._topic, messages }, topic.id)
|
||||
}
|
||||
})
|
||||
@ -1717,7 +1735,10 @@ const migrateConfig = {
|
||||
cutoffLimit: state.websearch.contentLimit
|
||||
}
|
||||
} else {
|
||||
state.websearch.compressionConfig = { method: 'none', cutoffUnit: 'char' }
|
||||
state.websearch.compressionConfig = {
|
||||
method: 'none',
|
||||
cutoffUnit: 'char'
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-ignore eslint-disable-next-line
|
||||
@ -2502,7 +2523,10 @@ const migrateConfig = {
|
||||
const cherryinProvider = state.llm.providers.find((provider) => provider.id === 'cherryin')
|
||||
|
||||
if (cherryinProvider) {
|
||||
updateProvider(state, 'cherryin', { apiHost: 'https://open.cherryin.ai', models: [] })
|
||||
updateProvider(state, 'cherryin', {
|
||||
apiHost: 'https://open.cherryin.ai',
|
||||
models: []
|
||||
})
|
||||
}
|
||||
|
||||
if (state.llm.defaultModel?.provider === 'cherryin') {
|
||||
@ -2571,9 +2595,36 @@ const migrateConfig = {
|
||||
return icon === 'agents' ? 'store' : icon
|
||||
})
|
||||
}
|
||||
state.llm.providers.forEach((provider) => {
|
||||
if (provider.anthropicApiHost) {
|
||||
return
|
||||
}
|
||||
|
||||
switch (provider.id) {
|
||||
case 'deepseek':
|
||||
provider.anthropicApiHost = 'https://api.deepseek.com/anthropic'
|
||||
break
|
||||
case 'moonshot':
|
||||
provider.anthropicApiHost = 'https://api.moonshot.cn/anthropic'
|
||||
break
|
||||
case 'zhipu':
|
||||
provider.anthropicApiHost = 'https://open.bigmodel.cn/api/anthropic'
|
||||
break
|
||||
case 'dashscope':
|
||||
provider.anthropicApiHost = 'https://dashscope.aliyuncs.com/api/v2/apps/claude-code-proxy'
|
||||
break
|
||||
case 'modelscope':
|
||||
provider.anthropicApiHost = 'https://api-inference.modelscope.cn'
|
||||
break
|
||||
case 'aihubmix':
|
||||
provider.anthropicApiHost = 'https://aihubmix.com/anthropic'
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
return state
|
||||
} catch (error) {
|
||||
logger.error('migrate 158 error', error as Error)
|
||||
logger.error('migrate 159 error', error as Error)
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,6 +13,7 @@ import type { FileMetadata } from './file'
|
||||
import { KnowledgeBase, KnowledgeReference } from './knowledge'
|
||||
import { MCPConfigSample, McpServerType } from './mcp'
|
||||
import type { Message } from './newMessage'
|
||||
import type { ServiceTier } from './provider'
|
||||
import type { BaseTool, MCPTool } from './tool'
|
||||
|
||||
export * from './agent'
|
||||
@ -251,6 +252,7 @@ export type Provider = {
|
||||
name: string
|
||||
apiKey: string
|
||||
apiHost: string
|
||||
anthropicApiHost?: string
|
||||
apiVersion?: string
|
||||
models: Model[]
|
||||
enabled?: boolean
|
||||
|
||||
Loading…
Reference in New Issue
Block a user