From cbf70fb299c4f065f54f26801543ac3f99f04765 Mon Sep 17 00:00:00 2001 From: Tao Yiping Date: Mon, 11 May 2026 11:51:28 +0800 Subject: [PATCH] =?UTF-8?q?feat(models):=20Added=20a=20Test=20button=20in?= =?UTF-8?q?=20Provider=20Settings=20that=20validates=E2=80=A6=20(#992)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Haze <709547807@qq.com> --- electron/shared/providers/registry.ts | 2 +- electron/shared/providers/types.ts | 3 +- src/components/settings/ProvidersSettings.tsx | 30 ++++++- src/i18n/locales/en/settings.json | 3 +- src/i18n/locales/ja/settings.json | 3 +- src/i18n/locales/ru/settings.json | 3 +- src/i18n/locales/zh/settings.json | 3 +- tests/e2e/provider-lifecycle.spec.ts | 78 +++++++++++++++++++ 8 files changed, 115 insertions(+), 10 deletions(-) diff --git a/electron/shared/providers/registry.ts b/electron/shared/providers/registry.ts index 377cbf7..9e4e896 100644 --- a/electron/shared/providers/registry.ts +++ b/electron/shared/providers/registry.ts @@ -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', diff --git a/electron/shared/providers/types.ts b/electron/shared/providers/types.ts index afa4c5e..5f25fc4 100644 --- a/electron/shared/providers/types.ts +++ b/electron/shared/providers/types.ts @@ -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' diff --git a/src/components/settings/ProvidersSettings.tsx b/src/components/settings/ProvidersSettings.tsx index 41bc6d4..a839adb 100644 --- a/src/components/settings/ProvidersSettings.tsx +++ b/src/components/settings/ProvidersSettings.tsx @@ -401,6 +401,7 @@ function ProviderCard({ const [validating, setValidating] = useState(false); const [saving, setSaving] = useState(false); const [arkMode, setArkMode] = useState('apikey'); + const [validationError, setValidationError] = useState(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 } = {}; 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({ 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({
setNewKey(e.target.value)} + onChange={(e) => { + setNewKey(e.target.value); + setValidationError(null); + }} className={cn(currentInputClasses, 'pr-10')} />
+ {validationError && ( +

+ + {t('aiProviders.dialog.failed')}: + {validationError} +

+ )}

{t('aiProviders.dialog.replaceApiKeyHelp')}

diff --git a/src/i18n/locales/en/settings.json b/src/i18n/locales/en/settings.json index 252ab9f..9b89c7e 100644 --- a/src/i18n/locales/en/settings.json +++ b/src/i18n/locales/en/settings.json @@ -72,7 +72,8 @@ "add": "Add Provider", "save": "Save", "customDoc": "Documentation", - "validate": "Validate" + "validate": "Validate", + "failed": "Failed" }, "card": { "default": "Default", diff --git a/src/i18n/locales/ja/settings.json b/src/i18n/locales/ja/settings.json index a731362..91e8eee 100644 --- a/src/i18n/locales/ja/settings.json +++ b/src/i18n/locales/ja/settings.json @@ -72,7 +72,8 @@ "add": "プロバイダーを追加", "save": "保存", "customDoc": "ドキュメント", - "validate": "検証" + "validate": "検証", + "failed": "失敗" }, "card": { "default": "デフォルト", diff --git a/src/i18n/locales/ru/settings.json b/src/i18n/locales/ru/settings.json index 1099033..c05d251 100644 --- a/src/i18n/locales/ru/settings.json +++ b/src/i18n/locales/ru/settings.json @@ -72,7 +72,8 @@ "add": "Добавить провайдера", "save": "Сохранить", "customDoc": "Документация", - "validate": "Проверить" + "validate": "Проверить", + "failed": "Ошибка" }, "card": { "default": "По умолчанию", diff --git a/src/i18n/locales/zh/settings.json b/src/i18n/locales/zh/settings.json index 8065529..a756ff2 100644 --- a/src/i18n/locales/zh/settings.json +++ b/src/i18n/locales/zh/settings.json @@ -72,7 +72,8 @@ "add": "添加提供商", "save": "保存", "customDoc": "查看文档", - "validate": "验证" + "validate": "验证", + "failed": "失败" }, "card": { "default": "默认", diff --git a/tests/e2e/provider-lifecycle.spec.ts b/tests/e2e/provider-lifecycle.spec.ts index 5a76c39..1f84365 100644 --- a/tests/e2e/provider-lifecycle.spec.ts +++ b/tests/e2e/provider-lifecycle.spec.ts @@ -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); + }); });