refactor(provider): provider API validation & CN defaults (#47)

This commit is contained in:
DigHuang
2026-02-10 21:48:15 -08:00
committed by GitHub
Unverified
parent 01f4d4800e
commit 0ced0b042c
6 changed files with 164 additions and 138 deletions

View File

@@ -42,6 +42,7 @@ import {
import { checkUvInstalled, installUv, setupManagedPython } from '../utils/uv-setup'; import { checkUvInstalled, installUv, setupManagedPython } from '../utils/uv-setup';
import { updateSkillConfig, getSkillConfig, getAllSkillConfigs } from '../utils/skill-config'; import { updateSkillConfig, getSkillConfig, getAllSkillConfigs } from '../utils/skill-config';
import { whatsAppLoginManager } from '../utils/whatsapp-login'; import { whatsAppLoginManager } from '../utils/whatsapp-login';
import { getProviderConfig } from '../utils/provider-registry';
/** /**
* Register all IPC handlers * Register all IPC handlers
@@ -900,9 +901,16 @@ function registerProviderHandlers(): void {
return await getDefaultProvider(); return await getDefaultProvider();
}); });
// Validate API key by making a real test request to the provider // Validate API key by making a real test request to the provider.
// providerId can be either a stored provider ID or a provider type (e.g., 'openrouter', 'anthropic') // providerId can be either a stored provider ID or a provider type.
ipcMain.handle('provider:validateKey', async (_, providerId: string, apiKey: string) => { ipcMain.handle(
'provider:validateKey',
async (
_,
providerId: string,
apiKey: string,
options?: { baseUrl?: string }
) => {
try { try {
// First try to get existing provider // First try to get existing provider
const provider = await getProvider(providerId); const provider = await getProvider(providerId);
@@ -910,50 +918,55 @@ function registerProviderHandlers(): void {
// Use provider.type if provider exists, otherwise use providerId as the type // Use provider.type if provider exists, otherwise use providerId as the type
// This allows validation during setup when provider hasn't been saved yet // This allows validation during setup when provider hasn't been saved yet
const providerType = provider?.type || providerId; const providerType = provider?.type || providerId;
const registryBaseUrl = getProviderConfig(providerType)?.baseUrl;
// Prefer caller-supplied baseUrl (live form value) over persisted config.
// This ensures Setup/Settings validation reflects unsaved edits immediately.
const resolvedBaseUrl = options?.baseUrl || provider?.baseUrl || registryBaseUrl;
console.log(`[clawx-validate] validating provider type: ${providerType}`); console.log(`[clawx-validate] validating provider type: ${providerType}`);
return await validateApiKeyWithProvider(providerType, apiKey); return await validateApiKeyWithProvider(providerType, apiKey, { baseUrl: resolvedBaseUrl });
} catch (error) { } catch (error) {
console.error('Validation error:', error); console.error('Validation error:', error);
return { valid: false, error: String(error) }; return { valid: false, error: String(error) };
} }
});
} }
);
}
type ValidationProfile = 'openai-compatible' | 'google-query-key' | 'anthropic-header' | 'none';
/** /**
* Validate API key using lightweight model-listing endpoints (zero token cost). * Validate API key using lightweight model-listing endpoints (zero token cost).
* Falls back to accepting the key for unknown/custom provider types. * Providers are grouped into 3 auth styles:
* - openai-compatible: Bearer auth + /models
* - google-query-key: ?key=... + /models
* - anthropic-header: x-api-key + anthropic-version + /models
*/ */
async function validateApiKeyWithProvider( async function validateApiKeyWithProvider(
providerType: string, providerType: string,
apiKey: string apiKey: string,
options?: { baseUrl?: string }
): Promise<{ valid: boolean; error?: string }> { ): Promise<{ valid: boolean; error?: string }> {
const profile = getValidationProfile(providerType);
if (profile === 'none') {
return { valid: true };
}
const trimmedKey = apiKey.trim(); const trimmedKey = apiKey.trim();
if (!trimmedKey) { if (!trimmedKey) {
return { valid: false, error: 'API key is required' }; return { valid: false, error: 'API key is required' };
} }
try { try {
switch (providerType) { switch (profile) {
case 'anthropic': case 'openai-compatible':
return await validateAnthropicKey(trimmedKey); return await validateOpenAiCompatibleKey(providerType, trimmedKey, options?.baseUrl);
case 'openai': case 'google-query-key':
return await validateOpenAIKey(trimmedKey); return await validateGoogleQueryKey(providerType, trimmedKey, options?.baseUrl);
case 'google': case 'anthropic-header':
return await validateGoogleKey(trimmedKey); return await validateAnthropicHeaderKey(providerType, trimmedKey, options?.baseUrl);
case 'openrouter':
return await validateOpenRouterKey(trimmedKey);
case 'moonshot':
return await validateMoonshotKey(trimmedKey);
case 'siliconflow':
return await validateSiliconFlowKey(trimmedKey);
case 'ollama':
// Ollama doesn't require API key validation
return { valid: true };
default: default:
// For custom providers, just check the key is not empty return { valid: false, error: `Unsupported validation profile for provider: ${providerType}` };
console.log(`[clawx-validate] ${providerType} uses local non-empty validation only`);
return { valid: true };
} }
} catch (error) { } catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error); const errorMessage = error instanceof Error ? error.message : String(error);
@@ -994,6 +1007,14 @@ function sanitizeHeaders(headers: Record<string, string>): Record<string, string
return next; return next;
} }
function normalizeBaseUrl(baseUrl: string): string {
return baseUrl.trim().replace(/\/+$/, '');
}
function buildOpenAiModelsUrl(baseUrl: string): string {
return `${normalizeBaseUrl(baseUrl)}/models?limit=1`;
}
function logValidationRequest( function logValidationRequest(
provider: string, provider: string,
method: string, method: string,
@@ -1005,6 +1026,38 @@ function logValidationRequest(
); );
} }
function getValidationProfile(providerType: string): ValidationProfile {
switch (providerType) {
case 'anthropic':
return 'anthropic-header';
case 'google':
return 'google-query-key';
case 'ollama':
return 'none';
default:
return 'openai-compatible';
}
}
async function performProviderValidationRequest(
providerLabel: string,
url: string,
headers: Record<string, string>
): Promise<{ valid: boolean; error?: string }> {
try {
logValidationRequest(providerLabel, 'GET', url, headers);
const response = await fetch(url, { headers });
logValidationStatus(providerLabel, response.status);
const data = await response.json().catch(() => ({}));
return classifyAuthResponse(response.status, data);
} catch (error) {
return {
valid: false,
error: `Connection error: ${error instanceof Error ? error.message : String(error)}`,
};
}
}
/** /**
* Helper: classify an HTTP response as valid / invalid / error. * Helper: classify an HTTP response as valid / invalid / error.
* 200 / 429 → valid (key works, possibly rate-limited). * 200 / 429 → valid (key works, possibly rate-limited).
@@ -1025,108 +1078,48 @@ function classifyAuthResponse(
return { valid: false, error: msg }; return { valid: false, error: msg };
} }
/** async function validateOpenAiCompatibleKey(
* Validate Anthropic API key via GET /v1/models (zero cost) providerType: string,
*/ apiKey: string,
async function validateAnthropicKey(apiKey: string): Promise<{ valid: boolean; error?: string }> { baseUrl?: string
try { ): Promise<{ valid: boolean; error?: string }> {
const url = 'https://api.anthropic.com/v1/models?limit=1'; const trimmedBaseUrl = baseUrl?.trim();
if (!trimmedBaseUrl) {
return { valid: false, error: `Base URL is required for provider "${providerType}" validation` };
}
const url = buildOpenAiModelsUrl(trimmedBaseUrl);
const headers = { Authorization: `Bearer ${apiKey}` };
return await performProviderValidationRequest(providerType, url, headers);
}
async function validateGoogleQueryKey(
providerType: string,
apiKey: string,
baseUrl?: string
): Promise<{ valid: boolean; error?: string }> {
const trimmedBaseUrl = baseUrl?.trim();
if (!trimmedBaseUrl) {
return { valid: false, error: `Base URL is required for provider "${providerType}" validation` };
}
const base = normalizeBaseUrl(trimmedBaseUrl);
const url = `${base}/models?pageSize=1&key=${encodeURIComponent(apiKey)}`;
return await performProviderValidationRequest(providerType, url, {});
}
async function validateAnthropicHeaderKey(
providerType: string,
apiKey: string,
baseUrl?: string
): Promise<{ valid: boolean; error?: string }> {
const base = normalizeBaseUrl(baseUrl || 'https://api.anthropic.com/v1');
const url = `${base}/models?limit=1`;
const headers = { const headers = {
'x-api-key': apiKey, 'x-api-key': apiKey,
'anthropic-version': '2023-06-01', 'anthropic-version': '2023-06-01',
}; };
logValidationRequest('anthropic', 'GET', url, headers); return await performProviderValidationRequest(providerType, url, headers);
const response = await fetch(url, { headers });
logValidationStatus('anthropic', response.status);
const data = await response.json().catch(() => ({}));
return classifyAuthResponse(response.status, data);
} catch (error) {
return { valid: false, error: `Connection error: ${error instanceof Error ? error.message : String(error)}` };
}
}
/**
* Validate OpenAI API key via GET /v1/models (zero cost)
*/
async function validateOpenAIKey(apiKey: string): Promise<{ valid: boolean; error?: string }> {
try {
const url = 'https://api.openai.com/v1/models?limit=1';
const headers = { Authorization: `Bearer ${apiKey}` };
logValidationRequest('openai', 'GET', url, headers);
const response = await fetch(url, { headers });
logValidationStatus('openai', response.status);
const data = await response.json().catch(() => ({}));
return classifyAuthResponse(response.status, data);
} catch (error) {
return { valid: false, error: `Connection error: ${error instanceof Error ? error.message : String(error)}` };
}
}
/**
* Validate Google (Gemini) API key via GET /v1beta/models (zero cost)
*/
async function validateGoogleKey(apiKey: string): Promise<{ valid: boolean; error?: string }> {
try {
const url = `https://generativelanguage.googleapis.com/v1beta/models?pageSize=1&key=${apiKey}`;
logValidationRequest('google', 'GET', url, {});
const response = await fetch(url);
logValidationStatus('google', response.status);
const data = await response.json().catch(() => ({}));
return classifyAuthResponse(response.status, data);
} catch (error) {
return { valid: false, error: `Connection error: ${error instanceof Error ? error.message : String(error)}` };
}
}
/**
* Validate OpenRouter API key via GET /api/v1/models (zero cost)
*/
async function validateOpenRouterKey(apiKey: string): Promise<{ valid: boolean; error?: string }> {
try {
const url = 'https://openrouter.ai/api/v1/models';
const headers = { Authorization: `Bearer ${apiKey}` };
logValidationRequest('openrouter', 'GET', url, headers);
const response = await fetch(url, { headers });
logValidationStatus('openrouter', response.status);
const data = await response.json().catch(() => ({}));
return classifyAuthResponse(response.status, data);
} catch (error) {
return { valid: false, error: `Connection error: ${error instanceof Error ? error.message : String(error)}` };
}
}
/**
* Validate Moonshot API key via GET /v1/models (zero cost)
*/
async function validateMoonshotKey(apiKey: string): Promise<{ valid: boolean; error?: string }> {
try {
const url = 'https://api.moonshot.cn/v1/models';
const headers = { Authorization: `Bearer ${apiKey}` };
logValidationRequest('moonshot', 'GET', url, headers);
const response = await fetch(url, { headers });
logValidationStatus('moonshot', response.status);
const data = await response.json().catch(() => ({}));
return classifyAuthResponse(response.status, data);
} catch (error) {
return { valid: false, error: `Connection error: ${error instanceof Error ? error.message : String(error)}` };
}
}
/**
* Validate SiliconFlow API key via GET /v1/models (zero cost)
*/
async function validateSiliconFlowKey(apiKey: string): Promise<{ valid: boolean; error?: string }> {
try {
const url = 'https://api.siliconflow.com/v1/models';
const headers = { Authorization: `Bearer ${apiKey}` };
logValidationRequest('siliconflow', 'GET', url, headers);
const response = await fetch(url, { headers });
logValidationStatus('siliconflow', response.status);
const data = await response.json().catch(() => ({}));
return classifyAuthResponse(response.status, data);
} catch (error) {
return { valid: false, error: `Connection error: ${error instanceof Error ? error.message : String(error)}` };
}
} }
/** /**

View File

@@ -92,7 +92,7 @@ const REGISTRY: Record<string, ProviderBackendMeta> = {
envVar: 'SILICONFLOW_API_KEY', envVar: 'SILICONFLOW_API_KEY',
defaultModel: 'siliconflow/deepseek-ai/DeepSeek-V3', defaultModel: 'siliconflow/deepseek-ai/DeepSeek-V3',
providerConfig: { providerConfig: {
baseUrl: 'https://api.siliconflow.com/v1', baseUrl: 'https://api.siliconflow.cn/v1',
api: 'openai-completions', api: 'openai-completions',
apiKeyEnv: 'SILICONFLOW_API_KEY', apiKeyEnv: 'SILICONFLOW_API_KEY',
}, },

View File

@@ -160,7 +160,7 @@ export function ProvidersSettings() {
); );
setEditingProvider(null); setEditingProvider(null);
}} }}
onValidateKey={(key) => validateApiKey(provider.id, key)} onValidateKey={(key, options) => validateApiKey(provider.id, key, options)}
/> />
))} ))}
</div> </div>
@@ -172,7 +172,7 @@ export function ProvidersSettings() {
existingTypes={new Set(providers.map((p) => p.type))} existingTypes={new Set(providers.map((p) => p.type))}
onClose={() => setShowAddDialog(false)} onClose={() => setShowAddDialog(false)}
onAdd={handleAddProvider} onAdd={handleAddProvider}
onValidateKey={(type, key) => validateApiKey(type, key)} onValidateKey={(type, key, options) => validateApiKey(type, key, options)}
/> />
)} )}
</div> </div>
@@ -189,7 +189,10 @@ interface ProviderCardProps {
onSetDefault: () => void; onSetDefault: () => void;
onToggleEnabled: () => void; onToggleEnabled: () => void;
onSaveEdits: (payload: { newApiKey?: string; updates?: Partial<ProviderConfig> }) => Promise<void>; onSaveEdits: (payload: { newApiKey?: string; updates?: Partial<ProviderConfig> }) => Promise<void>;
onValidateKey: (key: string) => Promise<{ valid: boolean; error?: string }>; onValidateKey: (
key: string,
options?: { baseUrl?: string }
) => Promise<{ valid: boolean; error?: string }>;
} }
/** /**
@@ -245,7 +248,9 @@ function ProviderCard({
if (newKey.trim()) { if (newKey.trim()) {
setValidating(true); setValidating(true);
const result = await onValidateKey(newKey); const result = await onValidateKey(newKey, {
baseUrl: baseUrl.trim() || undefined,
});
setValidating(false); setValidating(false);
if (!result.valid) { if (!result.valid) {
toast.error(result.error || 'Invalid API key'); toast.error(result.error || 'Invalid API key');
@@ -426,7 +431,11 @@ interface AddProviderDialogProps {
apiKey: string, apiKey: string,
options?: { baseUrl?: string; model?: string } options?: { baseUrl?: string; model?: string }
) => Promise<void>; ) => Promise<void>;
onValidateKey: (type: string, apiKey: string) => Promise<{ valid: boolean; error?: string }>; onValidateKey: (
type: string,
apiKey: string,
options?: { baseUrl?: string }
) => Promise<{ valid: boolean; error?: string }>;
} }
function AddProviderDialog({ existingTypes, onClose, onAdd, onValidateKey }: AddProviderDialogProps) { function AddProviderDialog({ existingTypes, onClose, onAdd, onValidateKey }: AddProviderDialogProps) {
@@ -461,7 +470,9 @@ function AddProviderDialog({ existingTypes, onClose, onAdd, onValidateKey }: Add
return; return;
} }
if (requiresKey && apiKey) { if (requiresKey && apiKey) {
const result = await onValidateKey(selectedType, apiKey); const result = await onValidateKey(selectedType, apiKey, {
baseUrl: baseUrl.trim() || undefined,
});
if (!result.valid) { if (!result.valid) {
setValidationError(result.error || 'Invalid API key'); setValidationError(result.error || 'Invalid API key');
setSaving(false); setSaving(false);

View File

@@ -59,8 +59,8 @@ export const PROVIDER_TYPE_INFO: ProviderTypeInfo[] = [
{ id: 'openai', name: 'OpenAI', icon: '💚', placeholder: 'sk-proj-...', model: 'GPT', requiresApiKey: true }, { id: 'openai', name: 'OpenAI', icon: '💚', placeholder: 'sk-proj-...', model: 'GPT', requiresApiKey: true },
{ id: 'google', name: 'Google', icon: '🔷', placeholder: 'AIza...', model: 'Gemini', requiresApiKey: true }, { id: 'google', name: 'Google', icon: '🔷', placeholder: 'AIza...', model: 'Gemini', requiresApiKey: true },
{ id: 'openrouter', name: 'OpenRouter', icon: '🌐', placeholder: 'sk-or-v1-...', model: 'Multi-Model', requiresApiKey: true }, { id: 'openrouter', name: 'OpenRouter', icon: '🌐', placeholder: 'sk-or-v1-...', model: 'Multi-Model', requiresApiKey: true },
{ id: 'moonshot', name: 'Moonshot', icon: '🌙', placeholder: 'sk-...', model: 'Kimi', requiresApiKey: true, defaultBaseUrl: 'https://api.moonshot.cn/v1', defaultModelId: 'kimi-k2.5' }, { id: 'moonshot', name: 'Moonshot (CN)', icon: '🌙', placeholder: 'sk-...', model: 'Kimi', requiresApiKey: true, defaultBaseUrl: 'https://api.moonshot.cn/v1', defaultModelId: 'kimi-k2.5' },
{ id: 'siliconflow', name: 'SiliconFlow', icon: '🌊', placeholder: 'sk-...', model: 'Multi-Model', requiresApiKey: true, defaultBaseUrl: 'https://api.siliconflow.com/v1', defaultModelId: 'moonshotai/Kimi-K2.5' }, { id: 'siliconflow', name: 'SiliconFlow (CN)', icon: '🌊', placeholder: 'sk-...', model: 'Multi-Model', requiresApiKey: true, defaultBaseUrl: 'https://api.siliconflow.cn/v1', defaultModelId: 'Pro/moonshotai/Kimi-K2.5' },
{ id: 'ollama', name: 'Ollama', icon: '🦙', placeholder: 'Not required', requiresApiKey: false, defaultBaseUrl: 'http://localhost:11434', showBaseUrl: true, showModelId: true, modelIdPlaceholder: 'qwen3:latest' }, { id: 'ollama', name: 'Ollama', icon: '🦙', placeholder: 'Not required', requiresApiKey: false, defaultBaseUrl: 'http://localhost:11434', showBaseUrl: true, showModelId: true, modelIdPlaceholder: 'qwen3:latest' },
{ id: 'custom', name: 'Custom', icon: '⚙️', placeholder: 'API key...', requiresApiKey: true, showBaseUrl: true, showModelId: true, modelIdPlaceholder: 'your-provider/model-id' }, { id: 'custom', name: 'Custom', icon: '⚙️', placeholder: 'API key...', requiresApiKey: true, showBaseUrl: true, showModelId: true, modelIdPlaceholder: 'your-provider/model-id' },
]; ];

View File

@@ -655,6 +655,7 @@ function ProviderContent({
const [showKey, setShowKey] = useState(false); const [showKey, setShowKey] = useState(false);
const [validating, setValidating] = useState(false); const [validating, setValidating] = useState(false);
const [keyValid, setKeyValid] = useState<boolean | null>(null); const [keyValid, setKeyValid] = useState<boolean | null>(null);
const [selectedProviderConfigId, setSelectedProviderConfigId] = useState<string | null>(null);
const [baseUrl, setBaseUrl] = useState(''); const [baseUrl, setBaseUrl] = useState('');
const [modelId, setModelId] = useState(''); const [modelId, setModelId] = useState('');
@@ -673,6 +674,7 @@ function ProviderContent({
|| setupCandidates[0]; || setupCandidates[0];
if (preferred && !cancelled) { if (preferred && !cancelled) {
onSelectProvider(preferred.type); onSelectProvider(preferred.type);
setSelectedProviderConfigId(preferred.id);
const typeInfo = providers.find((p) => p.id === preferred.type); const typeInfo = providers.find((p) => p.id === preferred.type);
const requiresKey = typeInfo?.requiresApiKey ?? false; const requiresKey = typeInfo?.requiresApiKey ?? false;
onConfiguredChange(!requiresKey || preferred.hasKey); onConfiguredChange(!requiresKey || preferred.hasKey);
@@ -706,6 +708,7 @@ function ProviderContent({
|| sameType.find((p) => p.hasKey) || sameType.find((p) => p.hasKey)
|| sameType[0]; || sameType[0];
const providerIdForLoad = preferredInstance?.id || selectedProvider; const providerIdForLoad = preferredInstance?.id || selectedProvider;
setSelectedProviderConfigId(providerIdForLoad);
const savedProvider = await window.electron.ipcRenderer.invoke( const savedProvider = await window.electron.ipcRenderer.invoke(
'provider:get', 'provider:get',
@@ -746,8 +749,9 @@ function ProviderContent({
if (requiresKey && apiKey) { if (requiresKey && apiKey) {
const result = await window.electron.ipcRenderer.invoke( const result = await window.electron.ipcRenderer.invoke(
'provider:validateKey', 'provider:validateKey',
selectedProvider, selectedProviderConfigId || selectedProvider,
apiKey apiKey,
{ baseUrl: baseUrl.trim() || undefined }
) as { valid: boolean; error?: string }; ) as { valid: boolean; error?: string };
setKeyValid(result.valid); setKeyValid(result.valid);
@@ -766,11 +770,18 @@ function ProviderContent({
modelId.trim() || modelId.trim() ||
undefined; undefined;
const providerIdForSave =
selectedProvider === 'custom'
? (selectedProviderConfigId?.startsWith('custom-')
? selectedProviderConfigId
: `custom-${crypto.randomUUID()}`)
: selectedProvider;
// Save provider config + API key, then set as default // Save provider config + API key, then set as default
const saveResult = await window.electron.ipcRenderer.invoke( const saveResult = await window.electron.ipcRenderer.invoke(
'provider:save', 'provider:save',
{ {
id: selectedProvider, id: providerIdForSave,
name: selectedProviderData?.name || selectedProvider, name: selectedProviderData?.name || selectedProvider,
type: selectedProvider, type: selectedProvider,
baseUrl: baseUrl.trim() || undefined, baseUrl: baseUrl.trim() || undefined,
@@ -788,13 +799,14 @@ function ProviderContent({
const defaultResult = await window.electron.ipcRenderer.invoke( const defaultResult = await window.electron.ipcRenderer.invoke(
'provider:setDefault', 'provider:setDefault',
selectedProvider providerIdForSave
) as { success: boolean; error?: string }; ) as { success: boolean; error?: string };
if (!defaultResult.success) { if (!defaultResult.success) {
throw new Error(defaultResult.error || 'Failed to set default provider'); throw new Error(defaultResult.error || 'Failed to set default provider');
} }
setSelectedProviderConfigId(providerIdForSave);
onConfiguredChange(true); onConfiguredChange(true);
toast.success('Provider configured'); toast.success('Provider configured');
} catch (error) { } catch (error) {
@@ -824,6 +836,7 @@ function ProviderContent({
onChange={(e) => { onChange={(e) => {
const val = e.target.value || null; const val = e.target.value || null;
onSelectProvider(val); onSelectProvider(val);
setSelectedProviderConfigId(null);
onConfiguredChange(false); onConfiguredChange(false);
onApiKeyChange(''); onApiKeyChange('');
setKeyValid(null); setKeyValid(null);

View File

@@ -27,7 +27,11 @@ interface ProviderState {
) => Promise<void>; ) => Promise<void>;
deleteApiKey: (providerId: string) => Promise<void>; deleteApiKey: (providerId: string) => Promise<void>;
setDefaultProvider: (providerId: string) => Promise<void>; setDefaultProvider: (providerId: string) => Promise<void>;
validateApiKey: (providerId: string, apiKey: string) => Promise<{ valid: boolean; error?: string }>; validateApiKey: (
providerId: string,
apiKey: string,
options?: { baseUrl?: string }
) => Promise<{ valid: boolean; error?: string }>;
getApiKey: (providerId: string) => Promise<string | null>; getApiKey: (providerId: string) => Promise<string | null>;
} }
@@ -188,9 +192,14 @@ export const useProviderStore = create<ProviderState>((set, get) => ({
} }
}, },
validateApiKey: async (providerId, apiKey) => { validateApiKey: async (providerId, apiKey, options) => {
try { try {
const result = await window.electron.ipcRenderer.invoke('provider:validateKey', providerId, apiKey) as { valid: boolean; error?: string }; const result = await window.electron.ipcRenderer.invoke(
'provider:validateKey',
providerId,
apiKey,
options
) as { valid: boolean; error?: string };
return result; return result;
} catch (error) { } catch (error) {
return { valid: false, error: String(error) }; return { valid: false, error: String(error) };