feat: add apiIdentifier setting for custom providers

This commit is contained in:
William Wang 2025-12-18 22:41:51 +08:00
parent 4571d01a92
commit 812028823a
4 changed files with 100 additions and 0 deletions

View File

@ -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"

View File

@ -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": "多个密钥使用逗号分隔"

View File

@ -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": "多個金鑰使用逗號分隔"

View File

@ -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<Props> = ({ providerId }) => {
const { provider, updateProvider, models } = useProvider(providerId)
const allProviders = useAllProviders()
@ -122,6 +124,7 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
const fancyProviderName = getFancyProviderName(provider)
const [localApiKey, setLocalApiKey] = useState(provider.apiKey)
const [apiIdentifier, setApiIdentifier] = useState(provider.apiIdentifier ?? '')
const [apiKeyConnectivity, setApiKeyConnectivity] = useState<ApiKeyConnectivity>({
status: HealthStatus.NOT_CHECKED,
checking: false
@ -147,6 +150,10 @@ const ProviderSetting: FC<Props> = ({ 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<Props> = ({ 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 (
<SettingContainer theme={theme} style={{ background: 'var(--color-background)' }}>
<SettingTitle>
@ -418,6 +460,34 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
/>
</SettingTitle>
<Divider style={{ width: '100%', margin: '10px 0' }} />
{!isSystemProvider(provider) && (
<>
<SettingSubtitle style={{ marginTop: 5, display: 'flex', alignItems: 'center', gap: 6 }}>
{t('settings.provider.api_identifier.label')}
<HelpTooltip title={t('settings.provider.api_identifier.tip')}></HelpTooltip>
</SettingSubtitle>
<Input
value={apiIdentifier}
placeholder={t('settings.provider.api_identifier.placeholder')}
onChange={(e) => setApiIdentifier(e.target.value)}
onBlur={onUpdateApiIdentifier}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.nativeEvent.isComposing) {
onUpdateApiIdentifier()
}
}}
spellCheck={false}
maxLength={32}
/>
<SettingHelpTextRow style={{ justifyContent: 'space-between' }}>
<SettingHelpText>
{t('settings.provider.api_identifier.preview', {
model: `${apiIdentifier.trim() || provider.id}:glm-4.6`
})}
</SettingHelpText>
</SettingHelpTextRow>
</>
)}
{isProviderSupportAuth(provider) && <ProviderOAuth providerId={provider.id} />}
{provider.id === 'openai' && <OpenAIAlert />}
{provider.id === 'ovms' && <OVMSSettings />}