diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index f4012363e3..25dd0b4b44 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -4469,6 +4469,16 @@ "api_host_no_valid": "API address is invalid", "api_host_preview": "Preview: {{url}}", "api_host_tooltip": "Override only when your provider requires a custom OpenAI-compatible endpoint.", + "api_identifier": { + "error": { + "duplicate": "Identifier is already used by another provider", + "invalid": "Only letters, numbers, '-' and '_' are allowed, and ':' is not allowed" + }, + "label": "API Identifier", + "placeholder": "Example: my-provider", + "preview": "Example: {{model}}", + "tip": "Used as the model prefix in API relay. Leave empty to use the internal UUID." + }, "api_key": { "label": "API Key", "tip": "Use commas to separate multiple keys" diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 0e5b2f60e7..a1dbfefc78 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -4469,6 +4469,16 @@ "api_host_no_valid": "API 地址不合法", "api_host_preview": "预览:{{url}}", "api_host_tooltip": "仅在服务商需要自定义的 OpenAI 兼容地址时覆盖。", + "api_identifier": { + "error": { + "duplicate": "该标识符已被其他提供商使用", + "invalid": "仅支持字母、数字、“-”、“_”,且不能包含“:”" + }, + "label": "API 标识符", + "placeholder": "例如:my-provider", + "preview": "示例:{{model}}", + "tip": "用于 API Relay 的模型前缀。留空则使用内部 UUID。" + }, "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 9625c68386..301077da62 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -4469,6 +4469,16 @@ "api_host_no_valid": "API 位址不合法", "api_host_preview": "預覽:{{url}}", "api_host_tooltip": "僅在供應商需要自訂的 OpenAI 相容端點時才覆蓋。", + "api_identifier": { + "error": { + "duplicate": "此識別碼已被其他供應商使用", + "invalid": "僅支援字母、數字、“-”、“_”,且不能包含“:”" + }, + "label": "API 識別碼", + "placeholder": "例如:my-provider", + "preview": "範例:{{model}}", + "tip": "用於 API Relay 的模型前綴。留空則使用內部 UUID。" + }, "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 049c14c0d1..981297cd7d 100644 --- a/src/renderer/src/pages/settings/ProviderSettings/ProviderSetting.tsx +++ b/src/renderer/src/pages/settings/ProviderSettings/ProviderSetting.tsx @@ -92,6 +92,8 @@ const isAnthropicCompatibleProviderId = (id: string): id is AnthropicCompatibleP type HostField = 'apiHost' | 'anthropicApiHost' +const API_IDENTIFIER_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,31}$/ + const ProviderSetting: FC = ({ providerId }) => { const { provider, updateProvider, models } = useProvider(providerId) const allProviders = useAllProviders() @@ -122,6 +124,7 @@ const ProviderSetting: FC = ({ providerId }) => { const fancyProviderName = getFancyProviderName(provider) const [localApiKey, setLocalApiKey] = useState(provider.apiKey) + const [apiIdentifier, setApiIdentifier] = useState(provider.apiIdentifier ?? '') const [apiKeyConnectivity, setApiKeyConnectivity] = useState({ status: HealthStatus.NOT_CHECKED, checking: false @@ -147,6 +150,10 @@ const ProviderSetting: FC = ({ providerId }) => { setApiKeyConnectivity({ status: HealthStatus.NOT_CHECKED }) }, [provider.apiKey]) + useEffect(() => { + setApiIdentifier(provider.apiIdentifier ?? '') + }, [provider.apiIdentifier]) + // 同步 localApiKey 到 provider.apiKey(防抖) useEffect(() => { if (localApiKey !== provider.apiKey) { @@ -385,6 +392,41 @@ const ProviderSetting: FC = ({ providerId }) => { const isAnthropicOAuth = () => provider.id === 'anthropic' && provider.authType === 'oauth' + const onUpdateApiIdentifier = useCallback(() => { + const normalizedIdentifier = apiIdentifier.trim() + + if (!normalizedIdentifier) { + updateProvider({ apiIdentifier: undefined }) + setApiIdentifier('') + return + } + + if (!API_IDENTIFIER_PATTERN.test(normalizedIdentifier) || normalizedIdentifier.includes(':')) { + window.toast.error(t('settings.provider.api_identifier.error.invalid')) + setApiIdentifier(provider.apiIdentifier ?? '') + return + } + + const conflictProvider = allProviders.find((p) => { + if (p.id === provider.id) { + return false + } + if (p.id === normalizedIdentifier) { + return true + } + return p.apiIdentifier?.trim() === normalizedIdentifier + }) + + if (conflictProvider) { + window.toast.error(t('settings.provider.api_identifier.error.duplicate')) + setApiIdentifier(provider.apiIdentifier ?? '') + return + } + + updateProvider({ apiIdentifier: normalizedIdentifier }) + setApiIdentifier(normalizedIdentifier) + }, [allProviders, apiIdentifier, provider.apiIdentifier, provider.id, t, updateProvider]) + return ( @@ -418,6 +460,34 @@ const ProviderSetting: FC = ({ providerId }) => { /> + {!isSystemProvider(provider) && ( + <> + + {t('settings.provider.api_identifier.label')} + + + setApiIdentifier(e.target.value)} + onBlur={onUpdateApiIdentifier} + onKeyDown={(e) => { + if (e.key === 'Enter' && !e.nativeEvent.isComposing) { + onUpdateApiIdentifier() + } + }} + spellCheck={false} + maxLength={32} + /> + + + {t('settings.provider.api_identifier.preview', { + model: `${apiIdentifier.trim() || provider.id}:glm-4.6` + })} + + + + )} {isProviderSupportAuth(provider) && } {provider.id === 'openai' && } {provider.id === 'ovms' && }