fix(llm): let custom OpenAI-compatible providers opt out of API Key requirement (#89)

This commit is contained in:
matevip 2026-05-09 14:26:35 +08:00
parent 88fb28a22a
commit d2886bdaa3
7 changed files with 32 additions and 3 deletions

View File

@ -12,5 +12,6 @@ public class CreateCustomProviderRequest {
private String apiKeyPrefix; private String apiKeyPrefix;
private String protocol; private String protocol;
private String chatModel; private String chatModel;
private Boolean requireApiKey;
private List<ModelInfoDTO> models; private List<ModelInfoDTO> models;
} }

View File

@ -11,6 +11,7 @@ public class ProviderConfigRequest {
private String protocol; private String protocol;
private String chatModel; private String chatModel;
private Map<String, Object> generateKwargs; private Map<String, Object> generateKwargs;
private Boolean requireApiKey;
/** /**
* RFC-009 P3.5: provider's position in the multi-model failover chain. * RFC-009 P3.5: provider's position in the multi-model failover chain.
* {@code 0} = excluded; positive ints define ascending try-order. When * {@code 0} = excluded; positive ints define ascending try-order. When

View File

@ -129,6 +129,9 @@ public class ModelProviderService {
provider.setBaseUrl(request.getBaseUrl()); provider.setBaseUrl(request.getBaseUrl());
provider.setChatModel(ModelProtocol.resolveChatModel(request.getProtocol(), request.getChatModel())); provider.setChatModel(ModelProtocol.resolveChatModel(request.getProtocol(), request.getChatModel()));
provider.setGenerateKwargs(writeJson(request.getGenerateKwargs())); provider.setGenerateKwargs(writeJson(request.getGenerateKwargs()));
if (request.getRequireApiKey() != null) {
provider.setRequireApiKey(request.getRequireApiKey());
}
// RFC-009 P3.5: only update fallback priority when the caller explicitly // RFC-009 P3.5: only update fallback priority when the caller explicitly
// sends a value. null leaves it untouched (existing chain unchanged). // sends a value. null leaves it untouched (existing chain unchanged).
if (request.getFallbackPriority() != null) { if (request.getFallbackPriority() != null) {
@ -169,7 +172,7 @@ public class ModelProviderService {
provider.setSupportModelDiscovery(false); provider.setSupportModelDiscovery(false);
provider.setSupportConnectionCheck(false); provider.setSupportConnectionCheck(false);
provider.setFreezeUrl(false); provider.setFreezeUrl(false);
provider.setRequireApiKey(true); provider.setRequireApiKey(request.getRequireApiKey() == null || Boolean.TRUE.equals(request.getRequireApiKey()));
modelProviderMapper.insert(provider); modelProviderMapper.insert(provider);
if (request.getModels() != null) { if (request.getModels() != null) {

View File

@ -608,6 +608,7 @@ export default {
protocolGemini: 'Gemini Native', protocolGemini: 'Gemini Native',
protocolDashScope: 'DashScope Native', protocolDashScope: 'DashScope Native',
advancedHint: 'Use this for generation options such as temperature, max_tokens, and top_p.', advancedHint: 'Use this for generation options such as temperature, max_tokens, and top_p.',
requireApiKeyHint: 'Turn this off for internal or local OpenAI-compatible services that do not require auth. Connection tests will omit the Authorization header.',
fallbackPriorityHint: 'Pool try-order (lower wins): 0 = excluded, 1 = first in line, 2 = second, and so on. Providers sharing the same value are tried in alphabetical order of their ID.', fallbackPriorityHint: 'Pool try-order (lower wins): 0 = excluded, 1 = first in line, 2 = second, and so on. Providers sharing the same value are tried in alphabetical order of their ID.',
fallbackBadge: 'Preferred #{priority}', fallbackBadge: 'Preferred #{priority}',
fallbackBadgeTitle: 'Position in the available pool — lower numbers are tried first', fallbackBadgeTitle: 'Position in the available pool — lower numbers are tried first',
@ -683,6 +684,7 @@ export default {
providerName: 'Provider Name', providerName: 'Provider Name',
defaultBaseUrl: 'Default Base URL', defaultBaseUrl: 'Default Base URL',
apiKeyPrefix: 'API Key Prefix', apiKeyPrefix: 'API Key Prefix',
requireApiKey: 'Requires API Key',
protocol: 'Protocol', protocol: 'Protocol',
generateKwargs: 'Generate Kwargs (JSON)', generateKwargs: 'Generate Kwargs (JSON)',
fallbackPriority: 'Pool Try-Order', fallbackPriority: 'Pool Try-Order',

View File

@ -494,6 +494,7 @@ export default {
protocolGemini: 'Gemini 原生', protocolGemini: 'Gemini 原生',
protocolDashScope: 'DashScope 原生', protocolDashScope: 'DashScope 原生',
advancedHint: '用于补充 temperature、max_tokens、top_p 等生成参数。', advancedHint: '用于补充 temperature、max_tokens、top_p 等生成参数。',
requireApiKeyHint: '公司内部或本地 OpenAI 兼容服务如果不需要鉴权,可以关闭此项;测试连接时将不会发送 Authorization 头。',
fallbackPriorityHint: '池内尝试顺序数字越小越先0 = 不参与1 = 第一顺位2 = 第二顺位,依此类推。多个提供商共用同一数字时按 ID 字典序。', fallbackPriorityHint: '池内尝试顺序数字越小越先0 = 不参与1 = 第一顺位2 = 第二顺位,依此类推。多个提供商共用同一数字时按 ID 字典序。',
fallbackBadge: '偏好 #{priority}', fallbackBadge: '偏好 #{priority}',
fallbackBadgeTitle: '可用池内的尝试顺序,数字越小越先尝试', fallbackBadgeTitle: '可用池内的尝试顺序,数字越小越先尝试',
@ -569,6 +570,7 @@ export default {
providerName: '提供商名称', providerName: '提供商名称',
defaultBaseUrl: '默认 Base URL', defaultBaseUrl: '默认 Base URL',
apiKeyPrefix: 'API Key 前缀', apiKeyPrefix: 'API Key 前缀',
requireApiKey: '需要 API Key',
protocol: '协议', protocol: '协议',
generateKwargs: 'Generate Kwargs (JSON)', generateKwargs: 'Generate Kwargs (JSON)',
fallbackPriority: '池内尝试顺序', fallbackPriority: '池内尝试顺序',

View File

@ -39,6 +39,7 @@ export function useProviderForm(deps: ListDeps) {
apiKeyPrefix: 'sk-', apiKeyPrefix: 'sk-',
protocol: 'openai-compatible', protocol: 'openai-compatible',
chatModel: 'OpenAIChatModel', chatModel: 'OpenAIChatModel',
requireApiKey: true,
generateKwargsText: '{}', generateKwargsText: '{}',
enableSearch: false, enableSearch: false,
searchStrategy: '', searchStrategy: '',
@ -110,6 +111,7 @@ export function useProviderForm(deps: ListDeps) {
apiKeyPrefix: 'sk-', apiKeyPrefix: 'sk-',
protocol: 'openai-compatible', protocol: 'openai-compatible',
chatModel: 'OpenAIChatModel', chatModel: 'OpenAIChatModel',
requireApiKey: true,
generateKwargsText: '{}', generateKwargsText: '{}',
enableSearch: false, enableSearch: false,
searchStrategy: '', searchStrategy: '',
@ -134,6 +136,7 @@ export function useProviderForm(deps: ListDeps) {
apiKeyPrefix: provider.apiKeyPrefix || 'sk-', apiKeyPrefix: provider.apiKeyPrefix || 'sk-',
protocol, protocol,
chatModel: provider.chatModel || 'OpenAIChatModel', chatModel: provider.chatModel || 'OpenAIChatModel',
requireApiKey: protocol === 'openai-compatible' ? provider.requireApiKey !== false : true,
generateKwargsText: JSON.stringify(kwargs, null, 2), generateKwargsText: JSON.stringify(kwargs, null, 2),
enableSearch: searchDefault, enableSearch: searchDefault,
searchStrategy: (kwargs.searchStrategy as string) || '', searchStrategy: (kwargs.searchStrategy as string) || '',
@ -175,12 +178,16 @@ export function useProviderForm(deps: ListDeps) {
} }
// RFC-009 P3.5: clamp to non-negative, coerce string input back to integer. // RFC-009 P3.5: clamp to non-negative, coerce string input back to integer.
const fallbackPriority = Math.max(0, Math.floor(Number(providerForm.fallbackPriority) || 0)) const fallbackPriority = Math.max(0, Math.floor(Number(providerForm.fallbackPriority) || 0))
const requireApiKey = providerForm.protocol === 'openai-compatible'
? providerForm.requireApiKey
: true
if (editingProvider.value) { if (editingProvider.value) {
await modelApi.updateProviderConfig(editingProvider.value.id, { await modelApi.updateProviderConfig(editingProvider.value.id, {
apiKey: providerForm.apiKey, apiKey: providerForm.apiKey,
baseUrl: providerForm.baseUrl, baseUrl: providerForm.baseUrl,
protocol: providerForm.protocol, protocol: providerForm.protocol,
chatModel: protocolToChatModel(providerForm.protocol), chatModel: protocolToChatModel(providerForm.protocol),
requireApiKey,
generateKwargs: kwargs, generateKwargs: kwargs,
fallbackPriority, fallbackPriority,
}) })
@ -192,6 +199,7 @@ export function useProviderForm(deps: ListDeps) {
apiKeyPrefix: providerForm.apiKeyPrefix, apiKeyPrefix: providerForm.apiKeyPrefix,
protocol: providerForm.protocol, protocol: providerForm.protocol,
chatModel: protocolToChatModel(providerForm.protocol), chatModel: protocolToChatModel(providerForm.protocol),
requireApiKey,
models: [], models: [],
}) })
if (providerForm.apiKey || providerForm.generateKwargsText || fallbackPriority > 0) { if (providerForm.apiKey || providerForm.generateKwargsText || fallbackPriority > 0) {
@ -200,6 +208,7 @@ export function useProviderForm(deps: ListDeps) {
baseUrl: providerForm.baseUrl, baseUrl: providerForm.baseUrl,
protocol: providerForm.protocol, protocol: providerForm.protocol,
chatModel: protocolToChatModel(providerForm.protocol), chatModel: protocolToChatModel(providerForm.protocol),
requireApiKey,
generateKwargs: kwargs, generateKwargs: kwargs,
fallbackPriority, fallbackPriority,
}) })

View File

@ -29,6 +29,16 @@
/> />
<div class="field-hint">{{ baseUrlHint }}</div> <div class="field-hint">{{ baseUrlHint }}</div>
</div> </div>
<div v-if="editingProvider?.authType !== 'oauth' && form.protocol === 'openai-compatible'" class="form-group">
<div class="search-toggle-row">
<label class="form-label" style="margin-bottom: 0">{{ t('settings.model.fields.requireApiKey') }}</label>
<label class="toggle-switch">
<input type="checkbox" v-model="form.requireApiKey" />
<span class="toggle-slider"></span>
</label>
</div>
<div class="field-hint">{{ t('settings.model.requireApiKeyHint') }}</div>
</div>
<!-- OAuth 登录区域auth_type === 'oauth' 时显示 --> <!-- OAuth 登录区域auth_type === 'oauth' 时显示 -->
<div v-if="editingProvider?.authType === 'oauth'" class="form-group full-width oauth-group"> <div v-if="editingProvider?.authType === 'oauth'" class="form-group full-width oauth-group">
<label class="form-label">{{ t('settings.model.oauthTitle') }}</label> <label class="form-label">{{ t('settings.model.oauthTitle') }}</label>
@ -65,7 +75,7 @@
</div> </div>
</div> </div>
<!-- API Key 输入区域 OAuth 时显示 --> <!-- API Key 输入区域 OAuth 时显示 -->
<div v-else class="form-group"> <div v-else-if="form.protocol !== 'openai-compatible' || form.requireApiKey" class="form-group">
<label class="form-label">{{ t('settings.model.apiKey') }}</label> <label class="form-label">{{ t('settings.model.apiKey') }}</label>
<input <input
v-model="form.apiKey" v-model="form.apiKey"
@ -76,7 +86,7 @@
/> />
<div class="field-hint">{{ t('settings.model.leaveBlankKeep') }}</div> <div class="field-hint">{{ t('settings.model.leaveBlankKeep') }}</div>
</div> </div>
<div class="form-group"> <div v-if="editingProvider?.authType !== 'oauth' && (form.protocol !== 'openai-compatible' || form.requireApiKey)" class="form-group">
<label class="form-label">{{ t('settings.model.fields.apiKeyPrefix') }}</label> <label class="form-label">{{ t('settings.model.fields.apiKeyPrefix') }}</label>
<input v-model="form.apiKeyPrefix" class="form-input" /> <input v-model="form.apiKeyPrefix" class="form-input" />
</div> </div>
@ -177,6 +187,7 @@ defineProps<{
apiKeyPrefix: string apiKeyPrefix: string
protocol: string protocol: string
chatModel: string chatModel: string
requireApiKey: boolean
generateKwargsText: string generateKwargsText: string
enableSearch: boolean enableSearch: boolean
searchStrategy: string searchStrategy: string