feat(models): Added a Test button in Provider Settings that validates… (#992)
Co-authored-by: Haze <709547807@qq.com>
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -72,7 +72,8 @@
|
||||
"add": "Add Provider",
|
||||
"save": "Save",
|
||||
"customDoc": "Documentation",
|
||||
"validate": "Validate"
|
||||
"validate": "Validate",
|
||||
"failed": "Failed"
|
||||
},
|
||||
"card": {
|
||||
"default": "Default",
|
||||
|
||||
@@ -72,7 +72,8 @@
|
||||
"add": "プロバイダーを追加",
|
||||
"save": "保存",
|
||||
"customDoc": "ドキュメント",
|
||||
"validate": "検証"
|
||||
"validate": "検証",
|
||||
"failed": "失敗"
|
||||
},
|
||||
"card": {
|
||||
"default": "デフォルト",
|
||||
|
||||
@@ -72,7 +72,8 @@
|
||||
"add": "Добавить провайдера",
|
||||
"save": "Сохранить",
|
||||
"customDoc": "Документация",
|
||||
"validate": "Проверить"
|
||||
"validate": "Проверить",
|
||||
"failed": "Ошибка"
|
||||
},
|
||||
"card": {
|
||||
"default": "По умолчанию",
|
||||
|
||||
@@ -72,7 +72,8 @@
|
||||
"add": "添加提供商",
|
||||
"save": "保存",
|
||||
"customDoc": "查看文档",
|
||||
"validate": "验证"
|
||||
"validate": "验证",
|
||||
"failed": "失败"
|
||||
},
|
||||
"card": {
|
||||
"default": "默认",
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user