diff --git a/src/renderer/src/aiCore/index_new.ts b/src/renderer/src/aiCore/index_new.ts index cf12a46aa6..ed92d4ddd8 100644 --- a/src/renderer/src/aiCore/index_new.ts +++ b/src/renderer/src/aiCore/index_new.ts @@ -27,6 +27,7 @@ import { buildAiSdkMiddlewares } from './middleware/AiSdkMiddlewareBuilder' import { buildPlugins } from './plugins/PluginBuilder' import { createAiSdkProvider } from './provider/factory' import { + adaptProvider, getActualProvider, isModernSdkSupported, prepareSpecialProviderConfig, @@ -64,12 +65,11 @@ export default class ModernAiProvider { * - URL will be automatically formatted via `formatProviderApiHost`, adding version suffixes like `/v1` * * 2. When called with `(model, provider)`: - * - **Directly uses the provided provider WITHOUT going through `getActualProvider`** - * - **URL will NOT be automatically formatted, `/v1` suffix will NOT be added** - * - This is legacy behavior kept for backward compatibility + * - The provided provider will be adapted via `adaptProvider` + * - URL formatting behavior depends on the adapted result * * 3. When called with `(provider)`: - * - Directly uses the provider without requiring a model + * - The provider will be adapted via `adaptProvider` * - Used for operations that don't need a model (e.g., fetchModels) * * @example @@ -77,7 +77,7 @@ export default class ModernAiProvider { * // Recommended: Auto-format URL * const ai = new ModernAiProvider(model) * - * // Not recommended: Skip URL formatting (only for special cases) + * // Provider will be adapted * const ai = new ModernAiProvider(model, customProvider) * * // For operations that don't need a model @@ -91,12 +91,12 @@ export default class ModernAiProvider { if (this.isModel(modelOrProvider)) { // 传入的是 Model this.model = modelOrProvider - this.actualProvider = provider || getActualProvider(modelOrProvider) + this.actualProvider = provider ? adaptProvider({ provider }) : getActualProvider(modelOrProvider) // 只保存配置,不预先创建executor this.config = providerToAiSdkConfig(this.actualProvider, modelOrProvider) } else { // 传入的是 Provider - this.actualProvider = modelOrProvider + this.actualProvider = adaptProvider({ provider: modelOrProvider }) // model为可选,某些操作(如fetchModels)不需要model } diff --git a/src/renderer/src/aiCore/provider/providerConfig.ts b/src/renderer/src/aiCore/provider/providerConfig.ts index 53194d3506..3ed3633d7c 100644 --- a/src/renderer/src/aiCore/provider/providerConfig.ts +++ b/src/renderer/src/aiCore/provider/providerConfig.ts @@ -78,11 +78,13 @@ function handleSpecialProviders(model: Model, provider: Provider): Provider { } /** - * 主要用来对齐AISdk的BaseURL格式 - * @param provider - * @returns + * Format and normalize the API host URL for a provider. + * Handles provider-specific URL formatting rules (e.g., appending version paths, Azure formatting). + * + * @param provider - The provider whose API host is to be formatted. + * @returns A new provider instance with the formatted API host. */ -function formatProviderApiHost(provider: Provider): Provider { +export function formatProviderApiHost(provider: Provider): Provider { const formatted = { ...provider } if (formatted.anthropicApiHost) { formatted.anthropicApiHost = formatApiHost(formatted.anthropicApiHost) @@ -114,18 +116,38 @@ function formatProviderApiHost(provider: Provider): Provider { } /** - * 获取实际的Provider配置 - * 简化版:将逻辑分解为小函数 + * Retrieve the effective Provider configuration for the given model. + * Applies all necessary transformations (special-provider handling, URL formatting, etc.). + * + * @param model - The model whose provider is to be resolved. + * @returns A new Provider instance with all adaptations applied. */ export function getActualProvider(model: Model): Provider { const baseProvider = getProviderByModel(model) - // 按顺序处理各种转换 - let actualProvider = cloneDeep(baseProvider) - actualProvider = handleSpecialProviders(model, actualProvider) - actualProvider = formatProviderApiHost(actualProvider) + return adaptProvider({ provider: baseProvider, model }) +} - return actualProvider +/** + * Transforms a provider configuration by applying model-specific adaptations and normalizing its API host. + * The transformations are applied in the following order: + * 1. Model-specific provider handling (e.g., New-API, system providers, Azure OpenAI) + * 2. API host formatting (provider-specific URL normalization) + * + * @param provider - The base provider configuration to transform. + * @param model - The model associated with the provider; optional but required for special-provider handling. + * @returns A new Provider instance with all transformations applied. + */ +export function adaptProvider({ provider, model }: { provider: Provider; model?: Model }): Provider { + let adaptedProvider = cloneDeep(provider) + + // Apply transformations in order + if (model) { + adaptedProvider = handleSpecialProviders(model, adaptedProvider) + } + adaptedProvider = formatProviderApiHost(adaptedProvider) + + return adaptedProvider } /** diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 782340e011..85e76b5cf4 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -4372,7 +4372,7 @@ "url": { "preview": "Preview: {{url}}", "reset": "Reset", - "tip": "ending with # forces use of input address" + "tip": "Add # at the end to disable the automatically appended API version." } }, "api_host": "API Host", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index c1874f7fb8..0ccfa0b16d 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -4372,7 +4372,7 @@ "url": { "preview": "预览: {{url}}", "reset": "重置", - "tip": "# 结尾强制使用输入地址" + "tip": "在末尾添加 # 以禁用自动附加的API版本。" } }, "api_host": "API 地址", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index db81e30006..d21f66ccb3 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -4372,7 +4372,7 @@ "url": { "preview": "預覽:{{url}}", "reset": "重設", - "tip": "# 結尾強制使用輸入位址" + "tip": "在末尾添加 # 以停用自動附加的 API 版本。" } }, "api_host": "API 主機地址", diff --git a/src/renderer/src/i18n/translate/de-de.json b/src/renderer/src/i18n/translate/de-de.json index e7314482a3..7d11af7a11 100644 --- a/src/renderer/src/i18n/translate/de-de.json +++ b/src/renderer/src/i18n/translate/de-de.json @@ -4372,7 +4372,7 @@ "url": { "preview": "Vorschau: {{url}}", "reset": "Zurücksetzen", - "tip": "# am Ende erzwingt die Verwendung der Eingabe-Adresse" + "tip": "Fügen Sie am Ende ein # hinzu, um die automatisch angehängte API-Version zu deaktivieren." } }, "api_host": "API-Adresse", diff --git a/src/renderer/src/i18n/translate/el-gr.json b/src/renderer/src/i18n/translate/el-gr.json index bc825ec688..f451e8af33 100644 --- a/src/renderer/src/i18n/translate/el-gr.json +++ b/src/renderer/src/i18n/translate/el-gr.json @@ -4372,7 +4372,7 @@ "url": { "preview": "Προεπισκόπηση: {{url}}", "reset": "Επαναφορά", - "tip": "#τέλος ενδεχόμενη χρήση της εισαγωγής διευθύνσεως" + "tip": "Προσθέστε το σύμβολο # στο τέλος για να απενεργοποιήσετε την αυτόματα προστιθέμενη έκδοση API." } }, "api_host": "Διεύθυνση API", diff --git a/src/renderer/src/i18n/translate/es-es.json b/src/renderer/src/i18n/translate/es-es.json index 2da83ad229..ee2b03d06b 100644 --- a/src/renderer/src/i18n/translate/es-es.json +++ b/src/renderer/src/i18n/translate/es-es.json @@ -4372,7 +4372,7 @@ "url": { "preview": "Vista previa: {{url}}", "reset": "Restablecer", - "tip": "forzar uso de dirección de entrada con # al final" + "tip": "Añada # al final para deshabilitar la versión de la API que se añade automáticamente." } }, "api_host": "Dirección API", diff --git a/src/renderer/src/i18n/translate/fr-fr.json b/src/renderer/src/i18n/translate/fr-fr.json index 3f3e9108c5..e909edc257 100644 --- a/src/renderer/src/i18n/translate/fr-fr.json +++ b/src/renderer/src/i18n/translate/fr-fr.json @@ -4372,7 +4372,7 @@ "url": { "preview": "Aperçu : {{url}}", "reset": "Réinitialiser", - "tip": "forcer l'utilisation de l'adresse d'entrée si terminé par #" + "tip": "Ajoutez # à la fin pour désactiver la version d'API ajoutée automatiquement." } }, "api_host": "Adresse API", diff --git a/src/renderer/src/i18n/translate/ja-jp.json b/src/renderer/src/i18n/translate/ja-jp.json index e2591fb20d..aa705da38d 100644 --- a/src/renderer/src/i18n/translate/ja-jp.json +++ b/src/renderer/src/i18n/translate/ja-jp.json @@ -4372,7 +4372,7 @@ "url": { "preview": "プレビュー: {{url}}", "reset": "リセット", - "tip": "#で終わる場合、入力されたアドレスを強制的に使用します" + "tip": "自動的に付加されるAPIバージョンを無効にするには、末尾に#を追加します。" } }, "api_host": "APIホスト", diff --git a/src/renderer/src/i18n/translate/pt-pt.json b/src/renderer/src/i18n/translate/pt-pt.json index cae6ebd38d..056306838e 100644 --- a/src/renderer/src/i18n/translate/pt-pt.json +++ b/src/renderer/src/i18n/translate/pt-pt.json @@ -4372,7 +4372,7 @@ "url": { "preview": "Pré-visualização: {{url}}", "reset": "Redefinir", - "tip": "e forçar o uso do endereço original quando terminar com '#'" + "tip": "Adicione # no final para desativar a versão da API adicionada automaticamente." } }, "api_host": "Endereço API", diff --git a/src/renderer/src/i18n/translate/ru-ru.json b/src/renderer/src/i18n/translate/ru-ru.json index fe5ebbcb25..12d696ec86 100644 --- a/src/renderer/src/i18n/translate/ru-ru.json +++ b/src/renderer/src/i18n/translate/ru-ru.json @@ -4372,7 +4372,7 @@ "url": { "preview": "Предпросмотр: {{url}}", "reset": "Сброс", - "tip": "заканчивая на # принудительно использует введенный адрес" + "tip": "Добавьте # в конце, чтобы отключить автоматически добавляемую версию API." } }, "api_host": "Хост API", diff --git a/src/renderer/src/pages/settings/ProviderSettings/ProviderSetting.tsx b/src/renderer/src/pages/settings/ProviderSettings/ProviderSetting.tsx index f341ac9229..da05409683 100644 --- a/src/renderer/src/pages/settings/ProviderSettings/ProviderSetting.tsx +++ b/src/renderer/src/pages/settings/ProviderSettings/ProviderSetting.tsx @@ -1,8 +1,10 @@ +import { adaptProvider } from '@renderer/aiCore/provider/providerConfig' import OpenAIAlert from '@renderer/components/Alert/OpenAIAlert' import { LoadingIcon } from '@renderer/components/Icons' import { HStack } from '@renderer/components/Layout' import { ApiKeyListPopup } from '@renderer/components/Popups/ApiKeyListPopup' import Selector from '@renderer/components/Selector' +import { HelpTooltip } from '@renderer/components/TooltipIcons' import { isEmbeddingModel, isRerankModel } from '@renderer/config/models' import { PROVIDER_URLS } from '@renderer/config/providers' import { useTheme } from '@renderer/context/ThemeProvider' @@ -19,14 +21,7 @@ import type { SystemProviderId } from '@renderer/types' import { isSystemProvider, isSystemProviderId, SystemProviderIds } from '@renderer/types' import type { ApiKeyConnectivity } from '@renderer/types/healthCheck' import { HealthStatus } from '@renderer/types/healthCheck' -import { - formatApiHost, - formatApiKeys, - formatAzureOpenAIApiHost, - formatVertexApiHost, - getFancyProviderName, - validateApiHost -} from '@renderer/utils' +import { formatApiHost, formatApiKeys, getFancyProviderName, validateApiHost } from '@renderer/utils' import { formatErrorMessage } from '@renderer/utils/error' import { isAIGatewayProvider, @@ -36,7 +31,6 @@ import { isNewApiProvider, isOpenAICompatibleProvider, isOpenAIProvider, - isSupportAPIVersionProvider, isVertexProvider } from '@renderer/utils/provider' import { Button, Divider, Flex, Input, Select, Space, Switch, Tooltip } from 'antd' @@ -281,12 +275,10 @@ const ProviderSetting: FC = ({ providerId }) => { }, [configuredApiHost, apiHost]) const hostPreview = () => { - if (apiHost.endsWith('#')) { - return apiHost.replace('#', '') - } + const formattedApiHost = adaptProvider({ provider: { ...provider, apiHost } }).apiHost if (isOpenAICompatibleProvider(provider)) { - return formatApiHost(apiHost, isSupportAPIVersionProvider(provider)) + '/chat/completions' + return formattedApiHost + '/chat/completions' } if (isAzureOpenAIProvider(provider)) { @@ -294,29 +286,26 @@ const ProviderSetting: FC = ({ providerId }) => { const path = !['preview', 'v1'].includes(apiVersion) ? `/v1/chat/completion?apiVersion=v1` : `/v1/responses?apiVersion=v1` - return formatAzureOpenAIApiHost(apiHost) + path + return formattedApiHost + path } if (isAnthropicProvider(provider)) { - // AI SDK uses the baseURL with /v1, then appends /messages - // formatApiHost adds /v1 automatically if not present - const normalizedHost = formatApiHost(apiHost) - return normalizedHost + '/messages' + return formattedApiHost + '/messages' } if (isGeminiProvider(provider)) { - return formatApiHost(apiHost, true, 'v1beta') + '/models' + return formattedApiHost + '/models' } if (isOpenAIProvider(provider)) { - return formatApiHost(apiHost) + '/responses' + return formattedApiHost + '/responses' } if (isVertexProvider(provider)) { - return formatVertexApiHost(provider) + '/publishers/google' + return formattedApiHost + '/publishers/google' } if (isAIGatewayProvider(provider)) { - return formatApiHost(apiHost) + '/language-model' + return formattedApiHost + '/language-model' } - return formatApiHost(apiHost) + return formattedApiHost } // API key 连通性检查状态指示器,目前仅在失败时显示 @@ -494,16 +483,21 @@ const ProviderSetting: FC = ({ providerId }) => { {!isDmxapi && ( <> - - setActiveHostField(value as HostField)} - options={hostSelectorOptions} - style={{ paddingLeft: 1, fontWeight: 'bold' }} - placement="bottomLeft" - /> - +
+ +
+ setActiveHostField(value as HostField)} + options={hostSelectorOptions} + style={{ paddingLeft: 1, fontWeight: 'bold' }} + placement="bottomLeft" + /> +
+
+ +