From 68974ffc683f7bbaa5a2b1c3ac207d4fdcab1202 Mon Sep 17 00:00:00 2001 From: Supra4E8C Date: Sun, 21 Dec 2025 23:46:39 +0800 Subject: [PATCH] feat(ai-providers): add prefix editing for provider configs --- src/i18n/locales/en.json | 4 +++ src/i18n/locales/zh-CN.json | 4 +++ src/pages/AiProvidersPage.tsx | 56 ++++++++++++++++++++++++++++++++ src/services/api/providers.ts | 3 ++ src/services/api/transformers.ts | 12 +++++++ src/types/provider.ts | 3 ++ 6 files changed, 82 insertions(+) diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 835e9dc..50f6bbb 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -29,6 +29,7 @@ "required": "Required", "api_key": "Key", "base_url": "Address", + "prefix": "Prefix", "proxy_url": "Proxy", "alias": "Alias", "failure": "Failure", @@ -171,6 +172,9 @@ "excluded_models_placeholder": "Comma or newline separated, e.g. gemini-1.5-pro, gemini-1.5-flash", "excluded_models_hint": "Leave empty to allow all models; values are trimmed and deduplicated automatically.", "excluded_models_count": "Excluding {{count}} models", + "prefix_label": "Prefix (Optional):", + "prefix_placeholder": "e.g.: team-a", + "prefix_hint": "When set, call models as prefix/ to target this entry.", "config_toggle_label": "Enabled", "config_disabled_badge": "Disabled", "codex_title": "Codex API Configuration", diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index 50e9569..cc6a4c2 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -29,6 +29,7 @@ "required": "必填", "api_key": "密钥", "base_url": "地址", + "prefix": "前缀", "proxy_url": "代理", "alias": "别名", "failure": "失败", @@ -171,6 +172,9 @@ "excluded_models_placeholder": "用逗号或换行分隔,例如: gemini-1.5-pro, gemini-1.5-flash", "excluded_models_hint": "留空表示不过滤;保存时会自动去重并忽略空白。", "excluded_models_count": "排除 {{count}} 个模型", + "prefix_label": "前缀 (可选):", + "prefix_placeholder": "例如: team-a", + "prefix_hint": "设置后可用 prefix/ 选择该条目。", "config_toggle_label": "启用", "config_disabled_badge": "已停用", "codex_title": "Codex API 配置", diff --git a/src/pages/AiProvidersPage.tsx b/src/pages/AiProvidersPage.tsx index 76af866..38eb4ee 100644 --- a/src/pages/AiProvidersPage.tsx +++ b/src/pages/AiProvidersPage.tsx @@ -39,6 +39,7 @@ interface ModelEntry { interface OpenAIFormState { name: string; + prefix: string; baseUrl: string; headers: HeaderEntry[]; testModel?: string; @@ -200,6 +201,7 @@ export function AiProvidersPage() { const [geminiForm, setGeminiForm] = useState({ apiKey: '', + prefix: '', baseUrl: '', headers: {}, excludedModels: [], @@ -209,6 +211,7 @@ export function AiProvidersPage() { ProviderKeyConfig & { modelEntries: ModelEntry[]; excludedText: string } >({ apiKey: '', + prefix: '', baseUrl: '', proxyUrl: '', headers: {}, @@ -219,6 +222,7 @@ export function AiProvidersPage() { }); const [openaiForm, setOpenaiForm] = useState({ name: '', + prefix: '', baseUrl: '', headers: [], apiKeyEntries: [buildApiKeyEntry()], @@ -317,6 +321,7 @@ export function AiProvidersPage() { setModal(null); setGeminiForm({ apiKey: '', + prefix: '', baseUrl: '', headers: {}, excludedModels: [], @@ -324,6 +329,7 @@ export function AiProvidersPage() { }); setProviderForm({ apiKey: '', + prefix: '', baseUrl: '', proxyUrl: '', headers: {}, @@ -334,6 +340,7 @@ export function AiProvidersPage() { }); setOpenaiForm({ name: '', + prefix: '', baseUrl: '', headers: [], apiKeyEntries: [buildApiKeyEntry()], @@ -410,6 +417,7 @@ export function AiProvidersPage() { const modelEntries = modelsToEntries(entry.models); setOpenaiForm({ name: entry.name, + prefix: entry.prefix ?? '', baseUrl: entry.baseUrl, headers: headersToEntries(entry.headers), testModel: entry.testModel, @@ -757,6 +765,7 @@ export function AiProvidersPage() { try { const payload: GeminiKeyConfig = { apiKey: geminiForm.apiKey.trim(), + prefix: geminiForm.prefix?.trim() || undefined, baseUrl: geminiForm.baseUrl?.trim() || undefined, headers: buildHeaderObject(headersToEntries(geminiForm.headers as any)), excludedModels: parseExcludedModels(geminiForm.excludedText), @@ -900,6 +909,7 @@ export function AiProvidersPage() { const payload: ProviderKeyConfig = { apiKey: providerForm.apiKey.trim(), + prefix: providerForm.prefix?.trim() || undefined, baseUrl, proxyUrl: providerForm.proxyUrl?.trim() || undefined, headers: buildHeaderObject(headersToEntries(providerForm.headers as any)), @@ -970,6 +980,7 @@ export function AiProvidersPage() { try { const payload: OpenAIProviderConfig = { name: openaiForm.name.trim(), + prefix: openaiForm.prefix?.trim() || undefined, baseUrl: openaiForm.baseUrl.trim(), headers: buildHeaderObject(openaiForm.headers), apiKeyEntries: openaiForm.apiKeyEntries.map((entry) => ({ @@ -1176,6 +1187,12 @@ export function AiProvidersPage() { {t('common.api_key')}: {maskApiKey(item.apiKey)} + {item.prefix && ( +
+ {t('common.prefix')}: + {item.prefix} +
+ )} {/* Base URL 行 */} {item.baseUrl && (
@@ -1274,6 +1291,12 @@ export function AiProvidersPage() { {t('common.api_key')}: {maskApiKey(item.apiKey)}
+ {item.prefix && ( +
+ {t('common.prefix')}: + {item.prefix} +
+ )} {/* Base URL 行 */} {item.baseUrl && (
@@ -1379,6 +1402,12 @@ export function AiProvidersPage() { {t('common.api_key')}: {maskApiKey(item.apiKey)}
+ {item.prefix && ( +
+ {t('common.prefix')}: + {item.prefix} +
+ )} {/* Base URL 行 */} {item.baseUrl && (
@@ -1567,6 +1596,12 @@ export function AiProvidersPage() { return (
{item.name}
+ {item.prefix && ( +
+ {t('common.prefix')}: + {item.prefix} +
+ )} {/* Base URL 行 */}
{t('common.base_url')}: @@ -1785,6 +1820,13 @@ export function AiProvidersPage() { value={geminiForm.apiKey} onChange={(e) => setGeminiForm((prev) => ({ ...prev, apiKey: e.target.value }))} /> + setGeminiForm((prev) => ({ ...prev, prefix: e.target.value }))} + hint={t('ai_providers.prefix_hint')} + /> setProviderForm((prev) => ({ ...prev, apiKey: e.target.value }))} /> + setProviderForm((prev) => ({ ...prev, prefix: e.target.value }))} + hint={t('ai_providers.prefix_hint')} + /> setOpenaiForm((prev) => ({ ...prev, name: e.target.value }))} /> + setOpenaiForm((prev) => ({ ...prev, prefix: e.target.value }))} + hint={t('ai_providers.prefix_hint')} + /> { const serializeProviderKey = (config: ProviderKeyConfig) => { const payload: Record = { 'api-key': config.apiKey }; + if (config.prefix?.trim()) payload.prefix = config.prefix.trim(); if (config.baseUrl) payload['base-url'] = config.baseUrl; if (config.proxyUrl) payload['proxy-url'] = config.proxyUrl; const headers = serializeHeaders(config.headers); @@ -62,6 +63,7 @@ const serializeProviderKey = (config: ProviderKeyConfig) => { const serializeGeminiKey = (config: GeminiKeyConfig) => { const payload: Record = { 'api-key': config.apiKey }; + if (config.prefix?.trim()) payload.prefix = config.prefix.trim(); if (config.baseUrl) payload['base-url'] = config.baseUrl; const headers = serializeHeaders(config.headers); if (headers) payload.headers = headers; @@ -79,6 +81,7 @@ const serializeOpenAIProvider = (provider: OpenAIProviderConfig) => { ? provider.apiKeyEntries.map((entry) => serializeApiKeyEntry(entry)) : [] }; + if (provider.prefix?.trim()) payload.prefix = provider.prefix.trim(); const headers = serializeHeaders(provider.headers); if (headers) payload.headers = headers; const models = serializeModelAliases(provider.models); diff --git a/src/services/api/transformers.ts b/src/services/api/transformers.ts index 852889a..8b8814d 100644 --- a/src/services/api/transformers.ts +++ b/src/services/api/transformers.ts @@ -70,6 +70,12 @@ const normalizeExcludedModels = (input: any): string[] => { return normalized; }; +const normalizePrefix = (value: any): string | undefined => { + if (value === undefined || value === null) return undefined; + const trimmed = String(value).trim(); + return trimmed ? trimmed : undefined; +}; + const normalizeApiKeyEntry = (entry: any): ApiKeyEntry | null => { if (!entry) return null; const apiKey = entry['api-key'] ?? entry.apiKey ?? entry.key ?? (typeof entry === 'string' ? entry : ''); @@ -93,6 +99,8 @@ const normalizeProviderKeyConfig = (item: any): ProviderKeyConfig | null => { if (!trimmed) return null; const config: ProviderKeyConfig = { apiKey: trimmed }; + const prefix = normalizePrefix(item.prefix ?? item['prefix']); + if (prefix) config.prefix = prefix; const baseUrl = item['base-url'] ?? item.baseUrl; const proxyUrl = item['proxy-url'] ?? item.proxyUrl; if (baseUrl) config.baseUrl = String(baseUrl); @@ -118,6 +126,8 @@ const normalizeGeminiKeyConfig = (item: any): GeminiKeyConfig | null => { if (!trimmed) return null; const config: GeminiKeyConfig = { apiKey: trimmed }; + const prefix = normalizePrefix(item.prefix ?? item['prefix']); + if (prefix) config.prefix = prefix; const baseUrl = item['base-url'] ?? item.baseUrl ?? item['base_url']; if (baseUrl) config.baseUrl = String(baseUrl); const headers = normalizeHeaders(item.headers); @@ -155,6 +165,8 @@ const normalizeOpenAIProvider = (provider: any): OpenAIProviderConfig | null => apiKeyEntries }; + const prefix = normalizePrefix(provider.prefix ?? provider['prefix']); + if (prefix) result.prefix = prefix; if (headers) result.headers = headers; if (models.length) result.models = models; if (priority !== undefined) result.priority = Number(priority); diff --git a/src/types/provider.ts b/src/types/provider.ts index 4442d55..85feacf 100644 --- a/src/types/provider.ts +++ b/src/types/provider.ts @@ -18,6 +18,7 @@ export interface ApiKeyEntry { export interface GeminiKeyConfig { apiKey: string; + prefix?: string; baseUrl?: string; headers?: Record; excludedModels?: string[]; @@ -25,6 +26,7 @@ export interface GeminiKeyConfig { export interface ProviderKeyConfig { apiKey: string; + prefix?: string; baseUrl?: string; proxyUrl?: string; headers?: Record; @@ -34,6 +36,7 @@ export interface ProviderKeyConfig { export interface OpenAIProviderConfig { name: string; + prefix?: string; baseUrl: string; apiKeyEntries: ApiKeyEntry[]; headers?: Record;