@@ -42,6 +42,7 @@ import {
4242import { checkUvInstalled , installUv , setupManagedPython } from '../utils/uv-setup' ;
4343import { updateSkillConfig , getSkillConfig , getAllSkillConfigs } from '../utils/skill-config' ;
4444import { whatsAppLoginManager } from '../utils/whatsapp-login' ;
45+ import { getProviderConfig } from '../utils/provider-registry' ;
4546
4647/**
4748 * Register all IPC handlers
@@ -900,60 +901,72 @@ function registerProviderHandlers(): void {
900901 return await getDefaultProvider ( ) ;
901902 } ) ;
902903
903- // Validate API key by making a real test request to the provider
904- // providerId can be either a stored provider ID or a provider type (e.g., 'openrouter', 'anthropic')
905- ipcMain . handle ( 'provider:validateKey' , async ( _ , providerId : string , apiKey : string ) => {
904+ // Validate API key by making a real test request to the provider.
905+ // providerId can be either a stored provider ID or a provider type.
906+ ipcMain . handle (
907+ 'provider:validateKey' ,
908+ async (
909+ _ ,
910+ providerId : string ,
911+ apiKey : string ,
912+ options ?: { baseUrl ?: string }
913+ ) => {
906914 try {
907915 // First try to get existing provider
908916 const provider = await getProvider ( providerId ) ;
909917
910918 // Use provider.type if provider exists, otherwise use providerId as the type
911919 // This allows validation during setup when provider hasn't been saved yet
912920 const providerType = provider ?. type || providerId ;
921+ const registryBaseUrl = getProviderConfig ( providerType ) ?. baseUrl ;
922+ // Prefer caller-supplied baseUrl (live form value) over persisted config.
923+ // This ensures Setup/Settings validation reflects unsaved edits immediately.
924+ const resolvedBaseUrl = options ?. baseUrl || provider ?. baseUrl || registryBaseUrl ;
913925
914926 console . log ( `[clawx-validate] validating provider type: ${ providerType } ` ) ;
915- return await validateApiKeyWithProvider ( providerType , apiKey ) ;
927+ return await validateApiKeyWithProvider ( providerType , apiKey , { baseUrl : resolvedBaseUrl } ) ;
916928 } catch ( error ) {
917929 console . error ( 'Validation error:' , error ) ;
918930 return { valid : false , error : String ( error ) } ;
919931 }
920- } ) ;
932+ }
933+ ) ;
921934}
922935
936+ type ValidationProfile = 'openai-compatible' | 'google-query-key' | 'anthropic-header' | 'none' ;
937+
923938/**
924939 * Validate API key using lightweight model-listing endpoints (zero token cost).
925- * Falls back to accepting the key for unknown/custom provider types.
940+ * Providers are grouped into 3 auth styles:
941+ * - openai-compatible: Bearer auth + /models
942+ * - google-query-key: ?key=... + /models
943+ * - anthropic-header: x-api-key + anthropic-version + /models
926944 */
927945async function validateApiKeyWithProvider (
928946 providerType : string ,
929- apiKey : string
947+ apiKey : string ,
948+ options ?: { baseUrl ?: string }
930949) : Promise < { valid : boolean ; error ?: string } > {
950+ const profile = getValidationProfile ( providerType ) ;
951+ if ( profile === 'none' ) {
952+ return { valid : true } ;
953+ }
954+
931955 const trimmedKey = apiKey . trim ( ) ;
932956 if ( ! trimmedKey ) {
933957 return { valid : false , error : 'API key is required' } ;
934958 }
935959
936960 try {
937- switch ( providerType ) {
938- case 'anthropic' :
939- return await validateAnthropicKey ( trimmedKey ) ;
940- case 'openai' :
941- return await validateOpenAIKey ( trimmedKey ) ;
942- case 'google' :
943- return await validateGoogleKey ( trimmedKey ) ;
944- case 'openrouter' :
945- return await validateOpenRouterKey ( trimmedKey ) ;
946- case 'moonshot' :
947- return await validateMoonshotKey ( trimmedKey ) ;
948- case 'siliconflow' :
949- return await validateSiliconFlowKey ( trimmedKey ) ;
950- case 'ollama' :
951- // Ollama doesn't require API key validation
952- return { valid : true } ;
961+ switch ( profile ) {
962+ case 'openai-compatible' :
963+ return await validateOpenAiCompatibleKey ( providerType , trimmedKey , options ?. baseUrl ) ;
964+ case 'google-query-key' :
965+ return await validateGoogleQueryKey ( providerType , trimmedKey , options ?. baseUrl ) ;
966+ case 'anthropic-header' :
967+ return await validateAnthropicHeaderKey ( providerType , trimmedKey , options ?. baseUrl ) ;
953968 default :
954- // For custom providers, just check the key is not empty
955- console . log ( `[clawx-validate] ${ providerType } uses local non-empty validation only` ) ;
956- return { valid : true } ;
969+ return { valid : false , error : `Unsupported validation profile for provider: ${ providerType } ` } ;
957970 }
958971 } catch ( error ) {
959972 const errorMessage = error instanceof Error ? error . message : String ( error ) ;
@@ -994,6 +1007,14 @@ function sanitizeHeaders(headers: Record<string, string>): Record<string, string
9941007 return next ;
9951008}
9961009
1010+ function normalizeBaseUrl ( baseUrl : string ) : string {
1011+ return baseUrl . trim ( ) . replace ( / \/ + $ / , '' ) ;
1012+ }
1013+
1014+ function buildOpenAiModelsUrl ( baseUrl : string ) : string {
1015+ return `${ normalizeBaseUrl ( baseUrl ) } /models?limit=1` ;
1016+ }
1017+
9971018function logValidationRequest (
9981019 provider : string ,
9991020 method : string ,
@@ -1005,6 +1026,38 @@ function logValidationRequest(
10051026 ) ;
10061027}
10071028
1029+ function getValidationProfile ( providerType : string ) : ValidationProfile {
1030+ switch ( providerType ) {
1031+ case 'anthropic' :
1032+ return 'anthropic-header' ;
1033+ case 'google' :
1034+ return 'google-query-key' ;
1035+ case 'ollama' :
1036+ return 'none' ;
1037+ default :
1038+ return 'openai-compatible' ;
1039+ }
1040+ }
1041+
1042+ async function performProviderValidationRequest (
1043+ providerLabel : string ,
1044+ url : string ,
1045+ headers : Record < string , string >
1046+ ) : Promise < { valid : boolean ; error ?: string } > {
1047+ try {
1048+ logValidationRequest ( providerLabel , 'GET' , url , headers ) ;
1049+ const response = await fetch ( url , { headers } ) ;
1050+ logValidationStatus ( providerLabel , response . status ) ;
1051+ const data = await response . json ( ) . catch ( ( ) => ( { } ) ) ;
1052+ return classifyAuthResponse ( response . status , data ) ;
1053+ } catch ( error ) {
1054+ return {
1055+ valid : false ,
1056+ error : `Connection error: ${ error instanceof Error ? error . message : String ( error ) } ` ,
1057+ } ;
1058+ }
1059+ }
1060+
10081061/**
10091062 * Helper: classify an HTTP response as valid / invalid / error.
10101063 * 200 / 429 → valid (key works, possibly rate-limited).
@@ -1025,108 +1078,48 @@ function classifyAuthResponse(
10251078 return { valid : false , error : msg } ;
10261079}
10271080
1028- /**
1029- * Validate Anthropic API key via GET /v1/models (zero cost)
1030- */
1031- async function validateAnthropicKey ( apiKey : string ) : Promise < { valid : boolean ; error ?: string } > {
1032- try {
1033- const url = 'https://api.anthropic.com/v1/models?limit=1' ;
1034- const headers = {
1035- 'x-api-key' : apiKey ,
1036- 'anthropic-version' : '2023-06-01' ,
1037- } ;
1038- logValidationRequest ( 'anthropic' , 'GET' , url , headers ) ;
1039- const response = await fetch ( url , { headers } ) ;
1040- logValidationStatus ( 'anthropic' , response . status ) ;
1041- const data = await response . json ( ) . catch ( ( ) => ( { } ) ) ;
1042- return classifyAuthResponse ( response . status , data ) ;
1043- } catch ( error ) {
1044- return { valid : false , error : `Connection error: ${ error instanceof Error ? error . message : String ( error ) } ` } ;
1045- }
1046- }
1047-
1048- /**
1049- * Validate OpenAI API key via GET /v1/models (zero cost)
1050- */
1051- async function validateOpenAIKey ( apiKey : string ) : Promise < { valid : boolean ; error ?: string } > {
1052- try {
1053- const url = 'https://api.openai.com/v1/models?limit=1' ;
1054- const headers = { Authorization : `Bearer ${ apiKey } ` } ;
1055- logValidationRequest ( 'openai' , 'GET' , url , headers ) ;
1056- const response = await fetch ( url , { headers } ) ;
1057- logValidationStatus ( 'openai' , response . status ) ;
1058- const data = await response . json ( ) . catch ( ( ) => ( { } ) ) ;
1059- return classifyAuthResponse ( response . status , data ) ;
1060- } catch ( error ) {
1061- return { valid : false , error : `Connection error: ${ error instanceof Error ? error . message : String ( error ) } ` } ;
1081+ async function validateOpenAiCompatibleKey (
1082+ providerType : string ,
1083+ apiKey : string ,
1084+ baseUrl ?: string
1085+ ) : Promise < { valid : boolean ; error ?: string } > {
1086+ const trimmedBaseUrl = baseUrl ?. trim ( ) ;
1087+ if ( ! trimmedBaseUrl ) {
1088+ return { valid : false , error : `Base URL is required for provider "${ providerType } " validation` } ;
10621089 }
1063- }
10641090
1065- /**
1066- * Validate Google (Gemini) API key via GET /v1beta/models (zero cost)
1067- */
1068- async function validateGoogleKey ( apiKey : string ) : Promise < { valid : boolean ; error ?: string } > {
1069- try {
1070- const url = `https://generativelanguage.googleapis.com/v1beta/models?pageSize=1&key=${ apiKey } ` ;
1071- logValidationRequest ( 'google' , 'GET' , url , { } ) ;
1072- const response = await fetch ( url ) ;
1073- logValidationStatus ( 'google' , response . status ) ;
1074- const data = await response . json ( ) . catch ( ( ) => ( { } ) ) ;
1075- return classifyAuthResponse ( response . status , data ) ;
1076- } catch ( error ) {
1077- return { valid : false , error : `Connection error: ${ error instanceof Error ? error . message : String ( error ) } ` } ;
1078- }
1091+ const url = buildOpenAiModelsUrl ( trimmedBaseUrl ) ;
1092+ const headers = { Authorization : `Bearer ${ apiKey } ` } ;
1093+ return await performProviderValidationRequest ( providerType , url , headers ) ;
10791094}
10801095
1081- /**
1082- * Validate OpenRouter API key via GET /api/v1/models (zero cost)
1083- */
1084- async function validateOpenRouterKey ( apiKey : string ) : Promise < { valid : boolean ; error ?: string } > {
1085- try {
1086- const url = 'https://openrouter.ai/api/v1/models' ;
1087- const headers = { Authorization : `Bearer ${ apiKey } ` } ;
1088- logValidationRequest ( 'openrouter' , 'GET' , url , headers ) ;
1089- const response = await fetch ( url , { headers } ) ;
1090- logValidationStatus ( 'openrouter' , response . status ) ;
1091- const data = await response . json ( ) . catch ( ( ) => ( { } ) ) ;
1092- return classifyAuthResponse ( response . status , data ) ;
1093- } catch ( error ) {
1094- return { valid : false , error : `Connection error: ${ error instanceof Error ? error . message : String ( error ) } ` } ;
1096+ async function validateGoogleQueryKey (
1097+ providerType : string ,
1098+ apiKey : string ,
1099+ baseUrl ?: string
1100+ ) : Promise < { valid : boolean ; error ?: string } > {
1101+ const trimmedBaseUrl = baseUrl ?. trim ( ) ;
1102+ if ( ! trimmedBaseUrl ) {
1103+ return { valid : false , error : `Base URL is required for provider "${ providerType } " validation` } ;
10951104 }
1096- }
10971105
1098- /**
1099- * Validate Moonshot API key via GET /v1/models (zero cost)
1100- */
1101- async function validateMoonshotKey ( apiKey : string ) : Promise < { valid : boolean ; error ?: string } > {
1102- try {
1103- const url = 'https://api.moonshot.cn/v1/models' ;
1104- const headers = { Authorization : `Bearer ${ apiKey } ` } ;
1105- logValidationRequest ( 'moonshot' , 'GET' , url , headers ) ;
1106- const response = await fetch ( url , { headers } ) ;
1107- logValidationStatus ( 'moonshot' , response . status ) ;
1108- const data = await response . json ( ) . catch ( ( ) => ( { } ) ) ;
1109- return classifyAuthResponse ( response . status , data ) ;
1110- } catch ( error ) {
1111- return { valid : false , error : `Connection error: ${ error instanceof Error ? error . message : String ( error ) } ` } ;
1112- }
1106+ const base = normalizeBaseUrl ( trimmedBaseUrl ) ;
1107+ const url = `${ base } /models?pageSize=1&key=${ encodeURIComponent ( apiKey ) } ` ;
1108+ return await performProviderValidationRequest ( providerType , url , { } ) ;
11131109}
11141110
1115- /**
1116- * Validate SiliconFlow API key via GET /v1/models (zero cost)
1117- */
1118- async function validateSiliconFlowKey ( apiKey : string ) : Promise < { valid : boolean ; error ?: string } > {
1119- try {
1120- const url = 'https://api.siliconflow.com/v1/models' ;
1121- const headers = { Authorization : `Bearer ${ apiKey } ` } ;
1122- logValidationRequest ( 'siliconflow' , 'GET' , url , headers ) ;
1123- const response = await fetch ( url , { headers } ) ;
1124- logValidationStatus ( 'siliconflow' , response . status ) ;
1125- const data = await response . json ( ) . catch ( ( ) => ( { } ) ) ;
1126- return classifyAuthResponse ( response . status , data ) ;
1127- } catch ( error ) {
1128- return { valid : false , error : `Connection error: ${ error instanceof Error ? error . message : String ( error ) } ` } ;
1129- }
1111+ async function validateAnthropicHeaderKey (
1112+ providerType : string ,
1113+ apiKey : string ,
1114+ baseUrl ?: string
1115+ ) : Promise < { valid : boolean ; error ?: string } > {
1116+ const base = normalizeBaseUrl ( baseUrl || 'https://api.anthropic.com/v1' ) ;
1117+ const url = `${ base } /models?limit=1` ;
1118+ const headers = {
1119+ 'x-api-key' : apiKey ,
1120+ 'anthropic-version' : '2023-06-01' ,
1121+ } ;
1122+ return await performProviderValidationRequest ( providerType , url , headers ) ;
11301123}
11311124
11321125/**
0 commit comments