feat(providers): implement real API key validation with OpenRouter support
- Replace mock API key validation with actual API calls to verify keys - Add validateApiKeyWithProvider() with provider-specific implementations - Support Anthropic, OpenAI, Google, and OpenRouter validation - Add OpenRouter as a new provider option in setup wizard and settings - Fix setup page to call real validation instead of mock length check - Allow validation during setup before provider is saved - Return user-friendly error messages instead of raw API errors
This commit is contained in:
87
build_process/commit_12_api_key_validation.md
Normal file
87
build_process/commit_12_api_key_validation.md
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
# Commit 12: Real API Key Validation & OpenRouter Support
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
Implemented real API key validation by making actual API calls to each provider, replacing the mock validation. Also added OpenRouter as a new provider option.
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
### 1. Real API Key Validation (`electron/main/ipc-handlers.ts`)
|
||||||
|
|
||||||
|
**Before**: Mock validation that only checked key format (e.g., `apiKey.startsWith('sk-ant-')`)
|
||||||
|
|
||||||
|
**After**: Real API validation that sends a minimal chat completion request to verify the key works
|
||||||
|
|
||||||
|
#### New Functions Added:
|
||||||
|
- `validateApiKeyWithProvider(providerType, apiKey)` - Routes to provider-specific validation
|
||||||
|
- `validateAnthropicKey(apiKey)` - Calls Anthropic `/v1/messages` endpoint
|
||||||
|
- `validateOpenAIKey(apiKey)` - Calls OpenAI `/v1/chat/completions` endpoint
|
||||||
|
- `validateGoogleKey(apiKey)` - Calls Google Gemini `generateContent` endpoint
|
||||||
|
- `validateOpenRouterKey(apiKey)` - Calls OpenRouter `/api/v1/chat/completions` endpoint
|
||||||
|
- `parseApiError(data)` - Extracts user-friendly error messages from API responses
|
||||||
|
|
||||||
|
#### Validation Logic:
|
||||||
|
- Sends minimal request with `max_tokens: 1` and message "hi"
|
||||||
|
- HTTP 200: Key is valid
|
||||||
|
- HTTP 401/403: Invalid API key
|
||||||
|
- HTTP 429: Rate limited but key is valid
|
||||||
|
- HTTP 402 (OpenRouter): No credits but key is valid
|
||||||
|
- HTTP 400/404: Check error message for auth vs model issues
|
||||||
|
|
||||||
|
#### Error Handling:
|
||||||
|
- Returns user-friendly "Invalid API key" instead of raw API errors like "User not found."
|
||||||
|
|
||||||
|
### 2. Setup Page Real Validation (`src/pages/Setup/index.tsx`)
|
||||||
|
|
||||||
|
**Before**:
|
||||||
|
```typescript
|
||||||
|
// Mock validation
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1500));
|
||||||
|
const isValid = apiKey.length > 10;
|
||||||
|
```
|
||||||
|
|
||||||
|
**After**:
|
||||||
|
```typescript
|
||||||
|
// Real API validation via IPC
|
||||||
|
const result = await window.electron.ipcRenderer.invoke(
|
||||||
|
'provider:validateKey',
|
||||||
|
selectedProvider,
|
||||||
|
apiKey
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. OpenRouter Provider Support
|
||||||
|
|
||||||
|
Added OpenRouter to:
|
||||||
|
- `src/pages/Setup/index.tsx` - Provider selection in setup wizard
|
||||||
|
- `src/components/settings/ProvidersSettings.tsx` - Provider settings panel
|
||||||
|
- `electron/utils/secure-storage.ts` - ProviderConfig type
|
||||||
|
- `src/stores/providers.ts` - ProviderConfig type
|
||||||
|
|
||||||
|
### 4. IPC Handler Improvement
|
||||||
|
|
||||||
|
Modified `provider:validateKey` handler to accept provider type directly:
|
||||||
|
- During setup, provider may not exist in storage yet
|
||||||
|
- Falls back to using `providerId` as the provider type
|
||||||
|
- Enables validation before provider is saved
|
||||||
|
|
||||||
|
## Files Changed
|
||||||
|
- `electron/main/ipc-handlers.ts` - Real API validation implementation (+300 lines)
|
||||||
|
- `src/pages/Setup/index.tsx` - Real validation call, OpenRouter option
|
||||||
|
- `src/components/settings/ProvidersSettings.tsx` - OpenRouter option
|
||||||
|
- `electron/utils/secure-storage.ts` - OpenRouter type
|
||||||
|
- `src/stores/providers.ts` - OpenRouter type
|
||||||
|
|
||||||
|
## API Endpoints Used
|
||||||
|
| Provider | Endpoint | Model |
|
||||||
|
|----------|----------|-------|
|
||||||
|
| Anthropic | `https://api.anthropic.com/v1/messages` | claude-3-haiku-20240307 |
|
||||||
|
| OpenAI | `https://api.openai.com/v1/chat/completions` | gpt-4o-mini |
|
||||||
|
| Google | `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent` | gemini-2.0-flash |
|
||||||
|
| OpenRouter | `https://openrouter.ai/api/v1/chat/completions` | meta-llama/llama-3.2-3b-instruct:free |
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
1. Select OpenRouter in setup wizard
|
||||||
|
2. Enter an invalid API key (e.g., "asdasfdsadf")
|
||||||
|
3. Click Validate - should show "Invalid API key"
|
||||||
|
4. Enter a valid API key
|
||||||
|
5. Click Validate - should show "API key validated successfully"
|
||||||
@@ -17,6 +17,7 @@
|
|||||||
* [commit_9] Skills browser - Bundles, categories, detail dialog
|
* [commit_9] Skills browser - Bundles, categories, detail dialog
|
||||||
* [commit_10] Cron tasks - Create/edit dialog, schedule presets, improved UI
|
* [commit_10] Cron tasks - Create/edit dialog, schedule presets, improved UI
|
||||||
* [commit_11] OpenClaw submodule fix - GitHub URL, auto-generated token, WebSocket auth
|
* [commit_11] OpenClaw submodule fix - GitHub URL, auto-generated token, WebSocket auth
|
||||||
|
* [commit_12] Real API key validation - OpenRouter support, actual API calls to verify keys
|
||||||
|
|
||||||
### Plan:
|
### Plan:
|
||||||
1. ~~Initialize project structure~~ ✅
|
1. ~~Initialize project structure~~ ✅
|
||||||
@@ -44,6 +45,7 @@ All core features have been implemented:
|
|||||||
- Skills browser with bundles
|
- Skills browser with bundles
|
||||||
- Cron tasks management for scheduled automation
|
- Cron tasks management for scheduled automation
|
||||||
- OpenClaw submodule from official GitHub (v2026.2.3) with auto-token auth
|
- OpenClaw submodule from official GitHub (v2026.2.3) with auto-token auth
|
||||||
|
- Real API key validation via actual API calls (Anthropic, OpenAI, Google, OpenRouter)
|
||||||
|
|
||||||
## Version Milestones
|
## Version Milestones
|
||||||
|
|
||||||
|
|||||||
@@ -266,45 +266,326 @@ function registerProviderHandlers(): void {
|
|||||||
return await getDefaultProvider();
|
return await getDefaultProvider();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Validate API key by making a test request (simulated for now)
|
// 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')
|
||||||
ipcMain.handle('provider:validateKey', async (_, providerId: string, apiKey: string) => {
|
ipcMain.handle('provider:validateKey', async (_, providerId: string, apiKey: string) => {
|
||||||
// In a real implementation, this would make a test API call to the provider
|
|
||||||
// For now, we'll just do basic format validation
|
|
||||||
try {
|
try {
|
||||||
// Basic validation based on provider type
|
// First try to get existing provider
|
||||||
const provider = await getProvider(providerId);
|
const provider = await getProvider(providerId);
|
||||||
if (!provider) {
|
|
||||||
return { valid: false, error: 'Provider not found' };
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (provider.type) {
|
// Use provider.type if provider exists, otherwise use providerId as the type
|
||||||
case 'anthropic':
|
// This allows validation during setup when provider hasn't been saved yet
|
||||||
if (!apiKey.startsWith('sk-ant-')) {
|
const providerType = provider?.type || providerId;
|
||||||
return { valid: false, error: 'Anthropic keys should start with sk-ant-' };
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'openai':
|
|
||||||
if (!apiKey.startsWith('sk-')) {
|
|
||||||
return { valid: false, error: 'OpenAI keys should start with sk-' };
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'google':
|
|
||||||
if (apiKey.length < 20) {
|
|
||||||
return { valid: false, error: 'Google API key seems too short' };
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Simulate API validation delay
|
console.log(`Validating API key for provider type: ${providerType}`);
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
return await validateApiKeyWithProvider(providerType, apiKey);
|
||||||
|
|
||||||
return { valid: true };
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error('Validation error:', error);
|
||||||
return { valid: false, error: String(error) };
|
return { valid: false, error: String(error) };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate API key by making a real chat completion API call to the provider
|
||||||
|
* This sends a minimal "hi" message to verify the key works
|
||||||
|
*/
|
||||||
|
async function validateApiKeyWithProvider(
|
||||||
|
providerType: string,
|
||||||
|
apiKey: string
|
||||||
|
): Promise<{ valid: boolean; error?: string }> {
|
||||||
|
const trimmedKey = apiKey.trim();
|
||||||
|
if (!trimmedKey) {
|
||||||
|
return { valid: false, error: 'API key is required' };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch (providerType) {
|
||||||
|
case 'anthropic':
|
||||||
|
return await validateAnthropicKey(trimmedKey);
|
||||||
|
case 'openai':
|
||||||
|
return await validateOpenAIKey(trimmedKey);
|
||||||
|
case 'google':
|
||||||
|
return await validateGoogleKey(trimmedKey);
|
||||||
|
case 'openrouter':
|
||||||
|
return await validateOpenRouterKey(trimmedKey);
|
||||||
|
case 'ollama':
|
||||||
|
// Ollama doesn't require API key validation
|
||||||
|
return { valid: true };
|
||||||
|
default:
|
||||||
|
// For custom providers, just check the key is not empty
|
||||||
|
return { valid: true };
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
return { valid: false, error: errorMessage };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse error message from API response
|
||||||
|
*/
|
||||||
|
function parseApiError(data: unknown): string {
|
||||||
|
if (!data || typeof data !== 'object') return 'Unknown error';
|
||||||
|
|
||||||
|
// Anthropic format: { error: { message: "..." } }
|
||||||
|
// OpenAI format: { error: { message: "..." } }
|
||||||
|
// Google format: { error: { message: "..." } }
|
||||||
|
const obj = data as { error?: { message?: string; type?: string }; message?: string };
|
||||||
|
|
||||||
|
if (obj.error?.message) return obj.error.message;
|
||||||
|
if (obj.message) return obj.message;
|
||||||
|
|
||||||
|
return 'Unknown error';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate Anthropic API key by making a minimal chat completion request
|
||||||
|
*/
|
||||||
|
async function validateAnthropicKey(apiKey: string): Promise<{ valid: boolean; error?: string }> {
|
||||||
|
try {
|
||||||
|
const response = await fetch('https://api.anthropic.com/v1/messages', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'x-api-key': apiKey,
|
||||||
|
'anthropic-version': '2023-06-01',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
model: 'claude-3-haiku-20240307',
|
||||||
|
max_tokens: 1,
|
||||||
|
messages: [{ role: 'user', content: 'hi' }],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json().catch(() => ({}));
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
return { valid: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authentication error
|
||||||
|
if (response.status === 401) {
|
||||||
|
return { valid: false, error: 'Invalid API key' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Permission error (invalid key format, etc.)
|
||||||
|
if (response.status === 403) {
|
||||||
|
return { valid: false, error: parseApiError(data) };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rate limit or overloaded - key is valid but service is busy
|
||||||
|
if (response.status === 429 || response.status === 529) {
|
||||||
|
return { valid: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Model not found or bad request but auth passed - key is valid
|
||||||
|
if (response.status === 400 || response.status === 404) {
|
||||||
|
const errorType = (data as { error?: { type?: string } })?.error?.type;
|
||||||
|
if (errorType === 'authentication_error' || errorType === 'invalid_api_key') {
|
||||||
|
return { valid: false, error: 'Invalid API key' };
|
||||||
|
}
|
||||||
|
// Other errors like invalid_request_error mean the key is valid
|
||||||
|
return { valid: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { valid: false, error: parseApiError(data) || `API error: ${response.status}` };
|
||||||
|
} catch (error) {
|
||||||
|
return { valid: false, error: `Connection error: ${error instanceof Error ? error.message : String(error)}` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate OpenAI API key by making a minimal chat completion request
|
||||||
|
*/
|
||||||
|
async function validateOpenAIKey(apiKey: string): Promise<{ valid: boolean; error?: string }> {
|
||||||
|
try {
|
||||||
|
const response = await fetch('https://api.openai.com/v1/chat/completions', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${apiKey}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
model: 'gpt-4o-mini',
|
||||||
|
max_tokens: 1,
|
||||||
|
messages: [{ role: 'user', content: 'hi' }],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json().catch(() => ({}));
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
return { valid: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authentication error
|
||||||
|
if (response.status === 401) {
|
||||||
|
return { valid: false, error: 'Invalid API key' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rate limit - key is valid
|
||||||
|
if (response.status === 429) {
|
||||||
|
return { valid: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Model not found or bad request but auth passed - key is valid
|
||||||
|
if (response.status === 400 || response.status === 404) {
|
||||||
|
const errorCode = (data as { error?: { code?: string } })?.error?.code;
|
||||||
|
if (errorCode === 'invalid_api_key') {
|
||||||
|
return { valid: false, error: 'Invalid API key' };
|
||||||
|
}
|
||||||
|
return { valid: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { valid: false, error: parseApiError(data) || `API error: ${response.status}` };
|
||||||
|
} catch (error) {
|
||||||
|
return { valid: false, error: `Connection error: ${error instanceof Error ? error.message : String(error)}` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate Google (Gemini) API key by making a minimal generate content request
|
||||||
|
*/
|
||||||
|
async function validateGoogleKey(apiKey: string): Promise<{ valid: boolean; error?: string }> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${apiKey}`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
contents: [{ parts: [{ text: 'hi' }] }],
|
||||||
|
generationConfig: { maxOutputTokens: 1 },
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const data = await response.json().catch(() => ({}));
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
return { valid: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authentication error
|
||||||
|
if (response.status === 400 || response.status === 401 || response.status === 403) {
|
||||||
|
const errorStatus = (data as { error?: { status?: string } })?.error?.status;
|
||||||
|
if (errorStatus === 'UNAUTHENTICATED' || errorStatus === 'PERMISSION_DENIED') {
|
||||||
|
return { valid: false, error: 'Invalid API key' };
|
||||||
|
}
|
||||||
|
// Check if it's actually an auth error
|
||||||
|
const errorMessage = parseApiError(data).toLowerCase();
|
||||||
|
if (errorMessage.includes('api key') || errorMessage.includes('invalid') || errorMessage.includes('unauthorized')) {
|
||||||
|
return { valid: false, error: parseApiError(data) };
|
||||||
|
}
|
||||||
|
// Other errors mean key is valid
|
||||||
|
return { valid: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rate limit - key is valid
|
||||||
|
if (response.status === 429) {
|
||||||
|
return { valid: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { valid: false, error: parseApiError(data) || `API error: ${response.status}` };
|
||||||
|
} catch (error) {
|
||||||
|
return { valid: false, error: `Connection error: ${error instanceof Error ? error.message : String(error)}` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate OpenRouter API key by making a minimal chat completion request
|
||||||
|
*/
|
||||||
|
async function validateOpenRouterKey(apiKey: string): Promise<{ valid: boolean; error?: string }> {
|
||||||
|
try {
|
||||||
|
// Use a popular free model for validation
|
||||||
|
const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${apiKey}`,
|
||||||
|
'HTTP-Referer': 'https://clawx.app',
|
||||||
|
'X-Title': 'ClawX',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
model: 'meta-llama/llama-3.2-3b-instruct:free',
|
||||||
|
max_tokens: 1,
|
||||||
|
messages: [{ role: 'user', content: 'hi' }],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json().catch(() => ({}));
|
||||||
|
console.log('OpenRouter validation response:', response.status, JSON.stringify(data));
|
||||||
|
|
||||||
|
// Helper to check if error message indicates auth failure
|
||||||
|
const isAuthError = (d: unknown): boolean => {
|
||||||
|
const errorObj = (d as { error?: { message?: string; code?: number | string; type?: string } })?.error;
|
||||||
|
if (!errorObj) return false;
|
||||||
|
|
||||||
|
const message = (errorObj.message || '').toLowerCase();
|
||||||
|
const code = errorObj.code;
|
||||||
|
const type = (errorObj.type || '').toLowerCase();
|
||||||
|
|
||||||
|
// Check for explicit auth-related errors
|
||||||
|
if (code === 401 || code === '401' || code === 403 || code === '403') return true;
|
||||||
|
if (type.includes('auth') || type.includes('invalid')) return true;
|
||||||
|
if (message.includes('invalid api key') || message.includes('invalid key') ||
|
||||||
|
message.includes('unauthorized') || message.includes('authentication') ||
|
||||||
|
message.includes('invalid credentials') || message.includes('api key is not valid')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
return { valid: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always check for auth errors in the response body first
|
||||||
|
if (isAuthError(data)) {
|
||||||
|
// Return user-friendly message instead of raw API errors like "User not found."
|
||||||
|
return { valid: false, error: 'Invalid API key' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authentication error status codes - always return user-friendly message
|
||||||
|
if (response.status === 401 || response.status === 403) {
|
||||||
|
return { valid: false, error: 'Invalid API key' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rate limit - key is valid
|
||||||
|
if (response.status === 429) {
|
||||||
|
return { valid: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Payment required or insufficient credits - key format is valid
|
||||||
|
if (response.status === 402) {
|
||||||
|
return { valid: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
// For 400/404, we must be very careful - only consider valid if clearly not an auth issue
|
||||||
|
if (response.status === 400 || response.status === 404) {
|
||||||
|
// If we got here without detecting auth error, it might be a model issue
|
||||||
|
// But be conservative - require explicit success indication
|
||||||
|
const errorObj = (data as { error?: { message?: string; code?: number } })?.error;
|
||||||
|
const message = (errorObj?.message || '').toLowerCase();
|
||||||
|
|
||||||
|
// Only consider valid if the error is clearly about the model, not the key
|
||||||
|
if (message.includes('model') && !message.includes('key') && !message.includes('auth')) {
|
||||||
|
return { valid: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default to invalid for ambiguous 400/404 errors
|
||||||
|
return { valid: false, error: parseApiError(data) || 'Invalid API key or request' };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { valid: false, error: parseApiError(data) || `API error: ${response.status}` };
|
||||||
|
} catch (error) {
|
||||||
|
return { valid: false, error: `Connection error: ${error instanceof Error ? error.message : String(error)}` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shell-related IPC handlers
|
* Shell-related IPC handlers
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ async function getProviderStore() {
|
|||||||
export interface ProviderConfig {
|
export interface ProviderConfig {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
type: 'anthropic' | 'openai' | 'google' | 'ollama' | 'custom';
|
type: 'anthropic' | 'openai' | 'google' | 'openrouter' | 'ollama' | 'custom';
|
||||||
baseUrl?: string;
|
baseUrl?: string;
|
||||||
model?: string;
|
model?: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ const providerTypes = [
|
|||||||
{ id: 'anthropic', name: 'Anthropic', icon: '🤖', placeholder: 'sk-ant-api03-...' },
|
{ id: 'anthropic', name: 'Anthropic', icon: '🤖', placeholder: 'sk-ant-api03-...' },
|
||||||
{ id: 'openai', name: 'OpenAI', icon: '💚', placeholder: 'sk-proj-...' },
|
{ id: 'openai', name: 'OpenAI', icon: '💚', placeholder: 'sk-proj-...' },
|
||||||
{ id: 'google', name: 'Google', icon: '🔷', placeholder: 'AIza...' },
|
{ id: 'google', name: 'Google', icon: '🔷', placeholder: 'AIza...' },
|
||||||
|
{ id: 'openrouter', name: 'OpenRouter', icon: '🌐', placeholder: 'sk-or-v1-...' },
|
||||||
{ id: 'ollama', name: 'Ollama', icon: '🦙', placeholder: 'Not required' },
|
{ id: 'ollama', name: 'Ollama', icon: '🦙', placeholder: 'Not required' },
|
||||||
{ id: 'custom', name: 'Custom', icon: '⚙️', placeholder: 'API key...' },
|
{ id: 'custom', name: 'Custom', icon: '⚙️', placeholder: 'API key...' },
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ const providers: Provider[] = [
|
|||||||
{ id: 'anthropic', name: 'Anthropic', model: 'Claude', icon: '🤖', placeholder: 'sk-ant-...' },
|
{ id: 'anthropic', name: 'Anthropic', model: 'Claude', icon: '🤖', placeholder: 'sk-ant-...' },
|
||||||
{ id: 'openai', name: 'OpenAI', model: 'GPT-4', icon: '💚', placeholder: 'sk-...' },
|
{ id: 'openai', name: 'OpenAI', model: 'GPT-4', icon: '💚', placeholder: 'sk-...' },
|
||||||
{ id: 'google', name: 'Google', model: 'Gemini', icon: '🔷', placeholder: 'AI...' },
|
{ id: 'google', name: 'Google', model: 'Gemini', icon: '🔷', placeholder: 'AI...' },
|
||||||
|
{ id: 'openrouter', name: 'OpenRouter', model: 'Multi-Model', icon: '🌐', placeholder: 'sk-or-...' },
|
||||||
];
|
];
|
||||||
|
|
||||||
// Channel types
|
// Channel types
|
||||||
@@ -593,18 +594,26 @@ function ProviderContent({
|
|||||||
setValidating(true);
|
setValidating(true);
|
||||||
setKeyValid(null);
|
setKeyValid(null);
|
||||||
|
|
||||||
// Simulate API key validation
|
try {
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1500));
|
// Call real API validation
|
||||||
|
const result = await window.electron.ipcRenderer.invoke(
|
||||||
// Basic validation - just check format
|
'provider:validateKey',
|
||||||
const isValid = apiKey.length > 10;
|
selectedProvider,
|
||||||
setKeyValid(isValid);
|
apiKey
|
||||||
setValidating(false);
|
) as { valid: boolean; error?: string };
|
||||||
|
|
||||||
if (isValid) {
|
setKeyValid(result.valid);
|
||||||
toast.success('API key validated successfully');
|
|
||||||
} else {
|
if (result.valid) {
|
||||||
toast.error('Invalid API key format');
|
toast.success('API key validated successfully');
|
||||||
|
} else {
|
||||||
|
toast.error(result.error || 'Invalid API key');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setKeyValid(false);
|
||||||
|
toast.error('Validation failed: ' + String(error));
|
||||||
|
} finally {
|
||||||
|
setValidating(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { create } from 'zustand';
|
|||||||
export interface ProviderConfig {
|
export interface ProviderConfig {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
type: 'anthropic' | 'openai' | 'google' | 'ollama' | 'custom';
|
type: 'anthropic' | 'openai' | 'google' | 'openrouter' | 'ollama' | 'custom';
|
||||||
baseUrl?: string;
|
baseUrl?: string;
|
||||||
model?: string;
|
model?: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
|
|||||||
Reference in New Issue
Block a user