feat(models): Added a Test button in Provider Settings that validates… (#992)

Co-authored-by: Haze <709547807@qq.com>
This commit is contained in:
Tao Yiping
2026-05-11 11:51:28 +08:00
committed by GitHub
parent 8a8cd73919
commit cbf70fb299
8 changed files with 115 additions and 10 deletions

View File

@@ -80,7 +80,7 @@ export const PROVIDER_DEFINITIONS: ProviderDefinition[] = [
supportsMultipleAccounts: true,
providerConfig: {
baseUrl: 'https://openrouter.ai/api/v1',
api: 'openai-completions',
api: 'openrouter',
apiKeyEnv: 'OPENROUTER_API_KEY',
headers: {
'HTTP-Referer': 'https://claw-x.com',

View File

@@ -37,7 +37,8 @@ export const OLLAMA_PLACEHOLDER_API_KEY = 'ollama-local';
export type ProviderProtocol =
| 'openai-completions'
| 'openai-responses'
| 'anthropic-messages';
| 'anthropic-messages'
| 'openrouter';
export type ProviderAuthMode =
| 'api_key'

View File

@@ -401,6 +401,7 @@ function ProviderCard({
const [validating, setValidating] = useState(false);
const [saving, setSaving] = useState(false);
const [arkMode, setArkMode] = useState<ArkMode>('apikey');
const [validationError, setValidationError] = useState<string | null>(null);
const typeInfo = PROVIDER_TYPE_INFO.find((t) => t.id === account.vendorId);
const providerDocsUrl = getProviderDocsUrl(typeInfo, i18n.language);
@@ -427,6 +428,7 @@ function ProviderCard({
setModelId(account.model || '');
setFallbackModelsText(normalizeFallbackModels(account.fallbackModels).join('\n'));
setFallbackProviderIds(normalizeFallbackProviderIds(account.fallbackAccountIds));
setValidationError(null);
setArkMode(
isArkCodePlanMode(
account.vendorId,
@@ -451,6 +453,7 @@ function ProviderCard({
const handleSaveEdits = async () => {
setSaving(true);
setValidationError(null);
try {
const payload: { newApiKey?: string; updates?: Partial<ProviderConfig> } = {};
const normalizedFallbackModels = normalizeFallbackModels(fallbackModelsText.split('\n'));
@@ -464,7 +467,7 @@ function ProviderCard({
});
setValidating(false);
if (!result.valid) {
toast.error(result.error || t('aiProviders.toast.invalidKey'));
setValidationError(result.error || t('aiProviders.toast.invalidKey'));
setSaving(false);
return;
}
@@ -473,7 +476,7 @@ function ProviderCard({
{
if (showModelIdField && !modelId.trim()) {
toast.error(t('aiProviders.toast.modelRequired'));
setValidationError(t('aiProviders.toast.modelRequired'));
setSaving(false);
return;
}
@@ -670,7 +673,10 @@ function ProviderCard({
<Label className={currentLabelClasses}>{t('aiProviders.dialog.modelId')}</Label>
<Input
value={modelId}
onChange={(e) => setModelId(e.target.value)}
onChange={(e) => {
setModelId(e.target.value);
setValidationError(null);
}}
placeholder={typeInfo?.modelIdPlaceholder || 'provider/model-id'}
className={currentInputClasses}
/>
@@ -851,10 +857,14 @@ function ProviderCard({
<div className="flex gap-2">
<div className="relative flex-1">
<Input
data-testid={`provider-edit-key-input-${account.id}`}
type={showKey ? 'text' : 'password'}
placeholder={typeInfo?.requiresApiKey ? typeInfo?.placeholder : (typeInfo?.id === 'ollama' ? t('aiProviders.notRequired') : t('aiProviders.card.editKey'))}
value={newKey}
onChange={(e) => setNewKey(e.target.value)}
onChange={(e) => {
setNewKey(e.target.value);
setValidationError(null);
}}
className={cn(currentInputClasses, 'pr-10')}
/>
<button
@@ -866,6 +876,7 @@ function ProviderCard({
</button>
</div>
<Button
data-testid={`provider-edit-save-${account.id}`}
variant="outline"
onClick={handleSaveEdits}
className={cn(
@@ -895,6 +906,7 @@ function ProviderCard({
)}
</Button>
<Button
data-testid={`provider-edit-cancel-${account.id}`}
variant="ghost"
onClick={onCancelEdit}
className={cn(
@@ -907,6 +919,16 @@ function ProviderCard({
<X className="h-4 w-4" />
</Button>
</div>
{validationError && (
<p
data-testid={`provider-edit-validation-error-${account.id}`}
className="text-xs text-red-500 flex items-center gap-1 mt-1"
>
<XCircle className="h-3 w-3 shrink-0" />
<span className="font-medium">{t('aiProviders.dialog.failed')}:</span>
<span>{validationError}</span>
</p>
)}
<p className="text-xs text-muted-foreground">
{t('aiProviders.dialog.replaceApiKeyHelp')}
</p>

View File

@@ -72,7 +72,8 @@
"add": "Add Provider",
"save": "Save",
"customDoc": "Documentation",
"validate": "Validate"
"validate": "Validate",
"failed": "Failed"
},
"card": {
"default": "Default",

View File

@@ -72,7 +72,8 @@
"add": "プロバイダーを追加",
"save": "保存",
"customDoc": "ドキュメント",
"validate": "検証"
"validate": "検証",
"failed": "失敗"
},
"card": {
"default": "デフォルト",

View File

@@ -72,7 +72,8 @@
"add": "Добавить провайдера",
"save": "Сохранить",
"customDoc": "Документация",
"validate": "Проверить"
"validate": "Проверить",
"failed": "Ошибка"
},
"card": {
"default": "По умолчанию",

View File

@@ -72,7 +72,8 @@
"add": "添加提供商",
"save": "保存",
"customDoc": "查看文档",
"validate": "验证"
"validate": "验证",
"failed": "失败"
},
"card": {
"default": "默认",

View File

@@ -162,4 +162,82 @@ test.describe('ClawX provider lifecycle', () => {
await expect(page.getByTestId('provider-card-custom')).toContainText('LM Studio Local');
});
test('edit form validates the new API key inline before saving (single button)', async ({ electronApp, page }) => {
await completeSetup(page);
await electronApp.evaluate(async ({ app: _app }) => {
const { ipcMain } = process.mainModule!.require('electron') as typeof import('electron');
const provider = {
id: 'moonshot-edit',
vendorId: 'moonshot',
label: 'Moonshot Edit',
authMode: 'api_key',
baseUrl: 'https://api.moonshot.cn/v1',
model: 'kimi-k2.6',
enabled: true,
isDefault: false,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
let storedKey = 'sk-existing';
let keyInfo = [{ accountId: provider.id, hasKey: true, keyMasked: 'sk-***' }];
const respond = (json: unknown, status = 200) => ({
ok: true,
data: {
status,
ok: status >= 200 && status < 300,
json,
},
});
ipcMain.removeHandler('hostapi:fetch');
ipcMain.handle('hostapi:fetch', async (_event: unknown, request: { path?: string; method?: string; body?: string | null }) => {
const path = request?.path ?? '';
const method = request?.method ?? 'GET';
const body = request?.body ? JSON.parse(request.body) : null;
if (path === '/api/provider-accounts' && method === 'GET') return respond([provider]);
if (path === '/api/provider-accounts/key-info' && method === 'GET') return respond(keyInfo);
if (path === '/api/provider-vendors' && method === 'GET') return respond([]);
if (path === '/api/provider-accounts/default' && method === 'GET') return respond({ accountId: provider.id });
if (path === '/api/provider-accounts/validate' && method === 'POST') {
if (body?.apiKey === 'sk-good') {
return respond({ valid: true });
}
return respond({ valid: false, error: 'Invalid API key' }, 400);
}
if (path.startsWith('/api/provider-accounts/') && method === 'PUT') {
if (body?.apiKey) storedKey = body.apiKey;
keyInfo = [{ accountId: provider.id, hasKey: Boolean(storedKey), keyMasked: 'sk-***' }];
return respond({ success: true });
}
if (path === '/api/providers' && method === 'GET') return respond([provider]);
return respond({});
});
});
await page.getByTestId('sidebar-nav-models').click();
await expect(page.getByTestId('providers-settings')).toBeVisible();
await expect(page.getByTestId('provider-card-moonshot-edit')).toBeVisible();
await page.getByTestId('provider-card-moonshot-edit').hover();
await page.getByTestId('provider-edit-moonshot-edit').click();
await page.getByTestId('provider-edit-key-input-moonshot-edit').fill('sk-bad');
await page.getByTestId('provider-edit-save-moonshot-edit').click();
await expect(page.getByTestId('provider-edit-validation-error-moonshot-edit')).toContainText('Invalid API key');
await page.getByTestId('provider-edit-key-input-moonshot-edit').fill('sk-good');
await expect(page.getByTestId('provider-edit-validation-error-moonshot-edit')).toHaveCount(0);
await page.getByTestId('provider-edit-save-moonshot-edit').click();
await expect(page.getByTestId('provider-edit-save-moonshot-edit')).toHaveCount(0);
});
});