mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-02-03 03:10:50 +08:00
feat: Added disabling features for some of the AI providers
This commit is contained in:
@@ -148,6 +148,8 @@
|
||||
"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",
|
||||
"config_toggle_label": "Enabled",
|
||||
"config_disabled_badge": "Disabled",
|
||||
"codex_title": "Codex API Configuration",
|
||||
"codex_add_button": "Add Configuration",
|
||||
"codex_empty_title": "No Codex Configuration",
|
||||
@@ -636,6 +638,8 @@
|
||||
"claude_config_added": "Claude configuration added successfully",
|
||||
"claude_config_updated": "Claude configuration updated successfully",
|
||||
"claude_config_deleted": "Claude configuration deleted successfully",
|
||||
"config_enabled": "Configuration enabled",
|
||||
"config_disabled": "Configuration disabled",
|
||||
"field_required": "Required fields cannot be empty",
|
||||
"openai_provider_required": "Please fill in provider name and Base URL",
|
||||
"openai_provider_added": "OpenAI provider added successfully",
|
||||
|
||||
@@ -54,6 +54,26 @@ interface AmpcodeFormState {
|
||||
mappingEntries: ModelEntry[];
|
||||
}
|
||||
|
||||
const DISABLE_ALL_MODELS_RULE = '*';
|
||||
|
||||
const hasDisableAllModelsRule = (models?: string[]) =>
|
||||
Array.isArray(models) && models.some((model) => String(model ?? '').trim() === DISABLE_ALL_MODELS_RULE);
|
||||
|
||||
const stripDisableAllModelsRule = (models?: string[]) =>
|
||||
Array.isArray(models)
|
||||
? models.filter((model) => String(model ?? '').trim() !== DISABLE_ALL_MODELS_RULE)
|
||||
: [];
|
||||
|
||||
const withDisableAllModelsRule = (models?: string[]) => {
|
||||
const base = stripDisableAllModelsRule(models);
|
||||
return [...base, DISABLE_ALL_MODELS_RULE];
|
||||
};
|
||||
|
||||
const withoutDisableAllModelsRule = (models?: string[]) => {
|
||||
const base = stripDisableAllModelsRule(models);
|
||||
return base;
|
||||
};
|
||||
|
||||
const parseExcludedModels = (text: string): string[] =>
|
||||
text
|
||||
.split(/[\n,]+/)
|
||||
@@ -213,6 +233,7 @@ export function AiProvidersPage() {
|
||||
const [openaiTestStatus, setOpenaiTestStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
|
||||
const [openaiTestMessage, setOpenaiTestMessage] = useState('');
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [configSwitchingKey, setConfigSwitchingKey] = useState<string | null>(null);
|
||||
|
||||
const disableControls = useMemo(() => connectionStatus !== 'connected', [connectionStatus]);
|
||||
const filteredOpenaiDiscoveryModels = useMemo(() => {
|
||||
@@ -726,6 +747,90 @@ export function AiProvidersPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const setConfigEnabled = async (
|
||||
provider: 'gemini' | 'codex' | 'claude',
|
||||
index: number,
|
||||
enabled: boolean
|
||||
) => {
|
||||
if (provider === 'gemini') {
|
||||
const current = geminiKeys[index];
|
||||
if (!current) return;
|
||||
|
||||
const switchingKey = `${provider}:${current.apiKey}`;
|
||||
setConfigSwitchingKey(switchingKey);
|
||||
|
||||
const previousList = geminiKeys;
|
||||
const nextExcluded = enabled
|
||||
? withoutDisableAllModelsRule(current.excludedModels)
|
||||
: withDisableAllModelsRule(current.excludedModels);
|
||||
const nextItem: GeminiKeyConfig = { ...current, excludedModels: nextExcluded };
|
||||
const nextList = previousList.map((item, idx) => (idx === index ? nextItem : item));
|
||||
|
||||
setGeminiKeys(nextList);
|
||||
updateConfigValue('gemini-api-key', nextList);
|
||||
clearCache('gemini-api-key');
|
||||
|
||||
try {
|
||||
await providersApi.saveGeminiKeys(nextList);
|
||||
showNotification(enabled ? t('notification.config_enabled') : t('notification.config_disabled'), 'success');
|
||||
} catch (err: any) {
|
||||
setGeminiKeys(previousList);
|
||||
updateConfigValue('gemini-api-key', previousList);
|
||||
clearCache('gemini-api-key');
|
||||
showNotification(`${t('notification.update_failed')}: ${err?.message || ''}`, 'error');
|
||||
} finally {
|
||||
setConfigSwitchingKey(null);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const source = provider === 'codex' ? codexConfigs : claudeConfigs;
|
||||
const current = source[index];
|
||||
if (!current) return;
|
||||
|
||||
const switchingKey = `${provider}:${current.apiKey}`;
|
||||
setConfigSwitchingKey(switchingKey);
|
||||
|
||||
const previousList = source;
|
||||
const nextExcluded = enabled
|
||||
? withoutDisableAllModelsRule(current.excludedModels)
|
||||
: withDisableAllModelsRule(current.excludedModels);
|
||||
const nextItem: ProviderKeyConfig = { ...current, excludedModels: nextExcluded };
|
||||
const nextList = previousList.map((item, idx) => (idx === index ? nextItem : item));
|
||||
|
||||
if (provider === 'codex') {
|
||||
setCodexConfigs(nextList);
|
||||
updateConfigValue('codex-api-key', nextList);
|
||||
clearCache('codex-api-key');
|
||||
} else {
|
||||
setClaudeConfigs(nextList);
|
||||
updateConfigValue('claude-api-key', nextList);
|
||||
clearCache('claude-api-key');
|
||||
}
|
||||
|
||||
try {
|
||||
if (provider === 'codex') {
|
||||
await providersApi.saveCodexConfigs(nextList);
|
||||
} else {
|
||||
await providersApi.saveClaudeConfigs(nextList);
|
||||
}
|
||||
showNotification(enabled ? t('notification.config_enabled') : t('notification.config_disabled'), 'success');
|
||||
} catch (err: any) {
|
||||
if (provider === 'codex') {
|
||||
setCodexConfigs(previousList);
|
||||
updateConfigValue('codex-api-key', previousList);
|
||||
clearCache('codex-api-key');
|
||||
} else {
|
||||
setClaudeConfigs(previousList);
|
||||
updateConfigValue('claude-api-key', previousList);
|
||||
clearCache('claude-api-key');
|
||||
}
|
||||
showNotification(`${t('notification.update_failed')}: ${err?.message || ''}`, 'error');
|
||||
} finally {
|
||||
setConfigSwitchingKey(null);
|
||||
}
|
||||
};
|
||||
|
||||
const saveProvider = async (type: 'codex' | 'claude') => {
|
||||
const baseUrl = (providerForm.baseUrl ?? '').trim();
|
||||
if (!baseUrl) {
|
||||
@@ -735,6 +840,8 @@ export function AiProvidersPage() {
|
||||
|
||||
setSaving(true);
|
||||
try {
|
||||
const source = type === 'codex' ? codexConfigs : claudeConfigs;
|
||||
|
||||
const payload: ProviderKeyConfig = {
|
||||
apiKey: providerForm.apiKey.trim(),
|
||||
baseUrl,
|
||||
@@ -744,7 +851,6 @@ export function AiProvidersPage() {
|
||||
excludedModels: parseExcludedModels(providerForm.excludedText)
|
||||
};
|
||||
|
||||
const source = type === 'codex' ? codexConfigs : claudeConfigs;
|
||||
const nextList =
|
||||
modal?.type === type && modal.index !== null
|
||||
? source.map((item, idx) => (idx === modal.index ? payload : item))
|
||||
@@ -908,7 +1014,11 @@ export function AiProvidersPage() {
|
||||
onEdit: (index: number) => void,
|
||||
onDelete: (item: T) => void,
|
||||
addLabel: string,
|
||||
deleteLabel?: string
|
||||
deleteLabel?: string,
|
||||
options?: {
|
||||
getRowDisabled?: (item: T, index: number) => boolean;
|
||||
renderExtraActions?: (item: T, index: number) => ReactNode;
|
||||
}
|
||||
) => {
|
||||
if (loading) {
|
||||
return <div className="hint">{t('common.loading')}</div>;
|
||||
@@ -930,19 +1040,33 @@ export function AiProvidersPage() {
|
||||
|
||||
return (
|
||||
<div className="item-list">
|
||||
{items.map((item, index) => (
|
||||
<div key={keyField(item)} className="item-row">
|
||||
<div className="item-meta">{renderContent(item, index)}</div>
|
||||
<div className="item-actions">
|
||||
<Button variant="secondary" size="sm" onClick={() => onEdit(index)} disabled={disableControls}>
|
||||
{t('common.edit')}
|
||||
</Button>
|
||||
<Button variant="danger" size="sm" onClick={() => onDelete(item)} disabled={disableControls}>
|
||||
{deleteLabel || t('common.delete')}
|
||||
</Button>
|
||||
{items.map((item, index) => {
|
||||
const rowDisabled = options?.getRowDisabled ? options.getRowDisabled(item, index) : false;
|
||||
return (
|
||||
<div key={keyField(item)} className="item-row" style={rowDisabled ? { opacity: 0.6 } : undefined}>
|
||||
<div className="item-meta">{renderContent(item, index)}</div>
|
||||
<div className="item-actions">
|
||||
{options?.renderExtraActions ? options.renderExtraActions(item, index) : null}
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => onEdit(index)}
|
||||
disabled={disableControls || saving || Boolean(configSwitchingKey)}
|
||||
>
|
||||
{t('common.edit')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="danger"
|
||||
size="sm"
|
||||
onClick={() => onDelete(item)}
|
||||
disabled={disableControls || saving || Boolean(configSwitchingKey)}
|
||||
>
|
||||
{deleteLabel || t('common.delete')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -954,7 +1078,11 @@ export function AiProvidersPage() {
|
||||
<Card
|
||||
title={t('ai_providers.gemini_title')}
|
||||
extra={
|
||||
<Button size="sm" onClick={() => openGeminiModal(null)} disabled={disableControls}>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => openGeminiModal(null)}
|
||||
disabled={disableControls || saving || Boolean(configSwitchingKey)}
|
||||
>
|
||||
{t('ai_providers.gemini_add_button')}
|
||||
</Button>
|
||||
}
|
||||
@@ -962,10 +1090,12 @@ export function AiProvidersPage() {
|
||||
{renderList<GeminiKeyConfig>(
|
||||
geminiKeys,
|
||||
(item) => item.apiKey,
|
||||
(item, index) => {
|
||||
const stats = getStatsBySource(item.apiKey, keyStats, maskApiKey);
|
||||
const headerEntries = Object.entries(item.headers || {});
|
||||
return (
|
||||
(item, index) => {
|
||||
const stats = getStatsBySource(item.apiKey, keyStats, maskApiKey);
|
||||
const headerEntries = Object.entries(item.headers || {});
|
||||
const configDisabled = hasDisableAllModelsRule(item.excludedModels);
|
||||
const excludedModels = item.excludedModels ?? [];
|
||||
return (
|
||||
<Fragment>
|
||||
<div className="item-title">
|
||||
{t('ai_providers.gemini_item_title')} #{index + 1}
|
||||
@@ -992,14 +1122,19 @@ export function AiProvidersPage() {
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{configDisabled && (
|
||||
<div className="status-badge warning" style={{ marginTop: 8, marginBottom: 0 }}>
|
||||
{t('ai_providers.config_disabled_badge')}
|
||||
</div>
|
||||
)}
|
||||
{/* 排除模型徽章 */}
|
||||
{item.excludedModels?.length ? (
|
||||
{excludedModels.length ? (
|
||||
<div className={styles.excludedModelsSection}>
|
||||
<div className={styles.excludedModelsLabel}>
|
||||
{t('ai_providers.excluded_models_count', { count: item.excludedModels.length })}
|
||||
{t('ai_providers.excluded_models_count', { count: excludedModels.length })}
|
||||
</div>
|
||||
<div className={styles.modelTagList}>
|
||||
{item.excludedModels.map((model) => (
|
||||
{excludedModels.map((model) => (
|
||||
<span key={model} className={`${styles.modelTag} ${styles.excludedModelTag}`}>
|
||||
<span className={styles.modelName}>{model}</span>
|
||||
</span>
|
||||
@@ -1021,14 +1156,30 @@ export function AiProvidersPage() {
|
||||
},
|
||||
(index) => openGeminiModal(index),
|
||||
(item) => deleteGemini(item.apiKey),
|
||||
t('ai_providers.gemini_add_button')
|
||||
t('ai_providers.gemini_add_button'),
|
||||
undefined,
|
||||
{
|
||||
getRowDisabled: (item) => hasDisableAllModelsRule(item.excludedModels),
|
||||
renderExtraActions: (item, index) => (
|
||||
<ToggleSwitch
|
||||
label={t('ai_providers.config_toggle_label')}
|
||||
checked={!hasDisableAllModelsRule(item.excludedModels)}
|
||||
disabled={disableControls || loading || saving || Boolean(configSwitchingKey)}
|
||||
onChange={(value) => void setConfigEnabled('gemini', index, value)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
title={t('ai_providers.codex_title')}
|
||||
extra={
|
||||
<Button size="sm" onClick={() => openProviderModal('codex', null)} disabled={disableControls}>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => openProviderModal('codex', null)}
|
||||
disabled={disableControls || saving || Boolean(configSwitchingKey)}
|
||||
>
|
||||
{t('ai_providers.codex_add_button')}
|
||||
</Button>
|
||||
}
|
||||
@@ -1036,10 +1187,12 @@ export function AiProvidersPage() {
|
||||
{renderList<ProviderKeyConfig>(
|
||||
codexConfigs,
|
||||
(item) => item.apiKey,
|
||||
(item, _index) => {
|
||||
const stats = getStatsBySource(item.apiKey, keyStats, maskApiKey);
|
||||
const headerEntries = Object.entries(item.headers || {});
|
||||
return (
|
||||
(item, _index) => {
|
||||
const stats = getStatsBySource(item.apiKey, keyStats, maskApiKey);
|
||||
const headerEntries = Object.entries(item.headers || {});
|
||||
const configDisabled = hasDisableAllModelsRule(item.excludedModels);
|
||||
const excludedModels = item.excludedModels ?? [];
|
||||
return (
|
||||
<Fragment>
|
||||
<div className="item-title">{t('ai_providers.codex_item_title')}</div>
|
||||
{/* API Key 行 */}
|
||||
@@ -1071,14 +1224,19 @@ export function AiProvidersPage() {
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{configDisabled && (
|
||||
<div className="status-badge warning" style={{ marginTop: 8, marginBottom: 0 }}>
|
||||
{t('ai_providers.config_disabled_badge')}
|
||||
</div>
|
||||
)}
|
||||
{/* 排除模型徽章 */}
|
||||
{item.excludedModels?.length ? (
|
||||
{excludedModels.length ? (
|
||||
<div className={styles.excludedModelsSection}>
|
||||
<div className={styles.excludedModelsLabel}>
|
||||
{t('ai_providers.excluded_models_count', { count: item.excludedModels.length })}
|
||||
{t('ai_providers.excluded_models_count', { count: excludedModels.length })}
|
||||
</div>
|
||||
<div className={styles.modelTagList}>
|
||||
{item.excludedModels.map((model) => (
|
||||
{excludedModels.map((model) => (
|
||||
<span key={model} className={`${styles.modelTag} ${styles.excludedModelTag}`}>
|
||||
<span className={styles.modelName}>{model}</span>
|
||||
</span>
|
||||
@@ -1100,14 +1258,30 @@ export function AiProvidersPage() {
|
||||
},
|
||||
(index) => openProviderModal('codex', index),
|
||||
(item) => deleteProviderEntry('codex', item.apiKey),
|
||||
t('ai_providers.codex_add_button')
|
||||
t('ai_providers.codex_add_button'),
|
||||
undefined,
|
||||
{
|
||||
getRowDisabled: (item) => hasDisableAllModelsRule(item.excludedModels),
|
||||
renderExtraActions: (item, index) => (
|
||||
<ToggleSwitch
|
||||
label={t('ai_providers.config_toggle_label')}
|
||||
checked={!hasDisableAllModelsRule(item.excludedModels)}
|
||||
disabled={disableControls || loading || saving || Boolean(configSwitchingKey)}
|
||||
onChange={(value) => void setConfigEnabled('codex', index, value)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
title={t('ai_providers.claude_title')}
|
||||
extra={
|
||||
<Button size="sm" onClick={() => openProviderModal('claude', null)} disabled={disableControls}>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => openProviderModal('claude', null)}
|
||||
disabled={disableControls || saving || Boolean(configSwitchingKey)}
|
||||
>
|
||||
{t('ai_providers.claude_add_button')}
|
||||
</Button>
|
||||
}
|
||||
@@ -1115,10 +1289,12 @@ export function AiProvidersPage() {
|
||||
{renderList<ProviderKeyConfig>(
|
||||
claudeConfigs,
|
||||
(item) => item.apiKey,
|
||||
(item, _index) => {
|
||||
const stats = getStatsBySource(item.apiKey, keyStats, maskApiKey);
|
||||
const headerEntries = Object.entries(item.headers || {});
|
||||
return (
|
||||
(item, _index) => {
|
||||
const stats = getStatsBySource(item.apiKey, keyStats, maskApiKey);
|
||||
const headerEntries = Object.entries(item.headers || {});
|
||||
const configDisabled = hasDisableAllModelsRule(item.excludedModels);
|
||||
const excludedModels = item.excludedModels ?? [];
|
||||
return (
|
||||
<Fragment>
|
||||
<div className="item-title">{t('ai_providers.claude_item_title')}</div>
|
||||
{/* API Key 行 */}
|
||||
@@ -1150,6 +1326,11 @@ export function AiProvidersPage() {
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{configDisabled && (
|
||||
<div className="status-badge warning" style={{ marginTop: 8, marginBottom: 0 }}>
|
||||
{t('ai_providers.config_disabled_badge')}
|
||||
</div>
|
||||
)}
|
||||
{/* 模型列表 */}
|
||||
{item.models?.length ? (
|
||||
<div className={styles.modelTagList}>
|
||||
@@ -1167,13 +1348,13 @@ export function AiProvidersPage() {
|
||||
</div>
|
||||
) : null}
|
||||
{/* 排除模型徽章 */}
|
||||
{item.excludedModels?.length ? (
|
||||
{excludedModels.length ? (
|
||||
<div className={styles.excludedModelsSection}>
|
||||
<div className={styles.excludedModelsLabel}>
|
||||
{t('ai_providers.excluded_models_count', { count: item.excludedModels.length })}
|
||||
{t('ai_providers.excluded_models_count', { count: excludedModels.length })}
|
||||
</div>
|
||||
<div className={styles.modelTagList}>
|
||||
{item.excludedModels.map((model) => (
|
||||
{excludedModels.map((model) => (
|
||||
<span key={model} className={`${styles.modelTag} ${styles.excludedModelTag}`}>
|
||||
<span className={styles.modelName}>{model}</span>
|
||||
</span>
|
||||
@@ -1195,14 +1376,30 @@ export function AiProvidersPage() {
|
||||
},
|
||||
(index) => openProviderModal('claude', index),
|
||||
(item) => deleteProviderEntry('claude', item.apiKey),
|
||||
t('ai_providers.claude_add_button')
|
||||
t('ai_providers.claude_add_button'),
|
||||
undefined,
|
||||
{
|
||||
getRowDisabled: (item) => hasDisableAllModelsRule(item.excludedModels),
|
||||
renderExtraActions: (item, index) => (
|
||||
<ToggleSwitch
|
||||
label={t('ai_providers.config_toggle_label')}
|
||||
checked={!hasDisableAllModelsRule(item.excludedModels)}
|
||||
disabled={disableControls || loading || saving || Boolean(configSwitchingKey)}
|
||||
onChange={(value) => void setConfigEnabled('claude', index, value)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
title={t('ai_providers.ampcode_title')}
|
||||
extra={
|
||||
<Button size="sm" onClick={openAmpcodeModal} disabled={disableControls}>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={openAmpcodeModal}
|
||||
disabled={disableControls || saving || ampcodeSaving || Boolean(configSwitchingKey)}
|
||||
>
|
||||
{t('common.edit')}
|
||||
</Button>
|
||||
}
|
||||
@@ -1259,7 +1456,11 @@ export function AiProvidersPage() {
|
||||
<Card
|
||||
title={t('ai_providers.openai_title')}
|
||||
extra={
|
||||
<Button size="sm" onClick={() => openOpenaiModal(null)} disabled={disableControls}>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => openOpenaiModal(null)}
|
||||
disabled={disableControls || saving || Boolean(configSwitchingKey)}
|
||||
>
|
||||
{t('ai_providers.openai_add_button')}
|
||||
</Button>
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user