diff --git a/packages/shared/anthropic/index.ts b/packages/shared/anthropic/index.ts index 4cbc78a8c6..251e634edf 100644 --- a/packages/shared/anthropic/index.ts +++ b/packages/shared/anthropic/index.ts @@ -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', diff --git a/src/renderer/src/aiCore/provider/providerConfig.ts b/src/renderer/src/aiCore/provider/providerConfig.ts index b91dad9cf7..ea97e0611f 100644 --- a/src/renderer/src/aiCore/provider/providerConfig.ts +++ b/src/renderer/src/aiCore/provider/providerConfig.ts @@ -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) diff --git a/src/renderer/src/config/providers.ts b/src/renderer/src/config/providers.ts index 543422d212..b0692e7f17 100644 --- a/src/renderer/src/config/providers.ts +++ b/src/renderer/src/config/providers.ts @@ -104,6 +104,7 @@ export const SYSTEM_PROVIDERS_CONFIG: Record = 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 = 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 = 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 = 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 = 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 = type: 'openai', apiKey: '', apiHost: 'https://api-inference.modelscope.cn/v1/', + anthropicApiHost: 'https://api-inference.modelscope.cn', models: SYSTEM_MODELS.modelscope, isSystem: true, enabled: false diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index a0c7dc7c67..e21a99c76d 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -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" diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 84f09d5d0a..2c41e60f12 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -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": "多个密钥使用逗号或空格分隔" diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 308da89ba9..ab2ebbacc4 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -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": "多個金鑰使用逗號或空格分隔" diff --git a/src/renderer/src/pages/settings/ProviderSettings/ProviderSetting.tsx b/src/renderer/src/pages/settings/ProviderSettings/ProviderSetting.tsx index ea40f6d9ac..bd3163163f 100644 --- a/src/renderer/src/pages/settings/ProviderSettings/ProviderSetting.tsx +++ b/src/renderer/src/pages/settings/ProviderSettings/ProviderSetting.tsx @@ -55,11 +55,14 @@ interface Props { providerId: string } +const ANTHROPIC_COMPATIBLE_PROVIDER_IDS = ['deepseek', 'moonshot', 'zhipu', 'dashscope', 'modelscope', 'aihubmix'] + const ProviderSetting: FC = ({ providerId }) => { const { provider, updateProvider, models } = useProvider(providerId) const allProviders = useAllProviders() const { updateProviders } = useProviders() const [apiHost, setApiHost] = useState(provider.apiHost) + const [anthropicApiHost, setAnthropicHost] = useState(provider.anthropicApiHost) const [apiVersion, setApiVersion] = useState(provider.apiVersion) const { t } = useTranslation() const { theme } = useTheme() @@ -140,6 +143,17 @@ const ProviderSetting: FC = ({ 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 = ({ 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 = ({ providerId }) => { {!isDmxapi && !isAnthropicOAuth() && ( <> - {t('settings.provider.api_host')} + + {t('settings.provider.api_host')} + )} + {(isOpenAIProvider(provider) || isAnthropicProvider(provider)) && ( - {hostPreview()} + {t('settings.provider.api_host_preview', { url: hostPreview() })} {t('settings.provider.api.url.tip')} )} + + {canConfigureAnthropicHost && ( + <> + + + {t('settings.provider.anthropic_api_host')} + + + + setAnthropicHost(e.target.value)} + onBlur={onUpdateAnthropicHost} + /> + + + + {t('settings.provider.anthropic_api_host_preview', { + url: anthropicHostPreview || '—' + })} + + + {t('settings.provider.anthropic_api_host_tip')} + + + + )} )} diff --git a/src/renderer/src/store/index.ts b/src/renderer/src/store/index.ts index aa8e8342ef..201108a7f3 100644 --- a/src/renderer/src/store/index.ts +++ b/src/renderer/src/store/index.ts @@ -65,7 +65,7 @@ const persistedReducer = persistReducer( { key: 'cherry-studio', storage, - version: 158, + version: 159, blacklist: ['runtime', 'messages', 'messageBlocks', 'tabs'], migrate }, diff --git a/src/renderer/src/store/migrate.ts b/src/renderer/src/store/migrate.ts index 80ee26d7c4..35dd216565 100644 --- a/src/renderer/src/store/migrate.ts +++ b/src/renderer/src/store/migrate.ts @@ -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 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 } } diff --git a/src/renderer/src/types/index.ts b/src/renderer/src/types/index.ts index 49a86cee9b..56bc883fe8 100644 --- a/src/renderer/src/types/index.ts +++ b/src/renderer/src/types/index.ts @@ -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