@@ -1290,6 +1290,7 @@ route("GET", "/ai/byok-test", handleBYOKTest);
12901290route ( "POST" , "/ai/byok-cache-clear" , handleBYOKCacheClear ) ;
12911291route ( "POST" , "/ai/byok-config" , handleBYOKConfigSave ) ;
12921292route ( "GET" , "/ai/byok-config" , handleBYOKConfigGet ) ;
1293+ route ( "POST" , "/ai/byok-validate" , handleBYOKValidate ) ;
12931294
12941295// ===============================================================
12951296// § 13. MAIN ENTRY POINT
@@ -7210,3 +7211,147 @@ async function handleBYOKConfigGet(request, env, ctx) {
72107211 has_api_key : true , // Don't expose the key itself
72117212 } ) ;
72127213}
7214+
7215+ /**
7216+ * POST /ai/byok-validate — Validate an API key and return available models.
7217+ * Body: { provider, api_key, base_url? }
7218+ * Returns: { connected, provider, models: [{ id, name, context_window?, recommended? }], error? }
7219+ */
7220+ async function handleBYOKValidate ( request , env , ctx ) {
7221+ requireAuth ( ctx ) ;
7222+ const body = await safeJson ( request ) ;
7223+ const { provider, api_key, base_url } = body ;
7224+
7225+ if ( ! provider || ! api_key ) {
7226+ throw new ValidationError ( "provider and api_key are required" ) ;
7227+ }
7228+
7229+ console . log ( `[BYOK-Validate] Testing connection: provider=${ provider } , keyPrefix=${ api_key . substring ( 0 , 8 ) } ...` ) ;
7230+
7231+ const RECOMMENDED = {
7232+ openai : [ "gpt-4o" , "gpt-4o-mini" , "gpt-4.1" , "gpt-4.1-mini" , "gpt-4.1-nano" , "o4-mini" ] ,
7233+ gemini : [ "gemini-2.5-flash" , "gemini-2.5-pro" , "gemini-2.0-flash" ] ,
7234+ anthropic : [ "claude-sonnet-4-20250514" , "claude-3-5-sonnet-20241022" , "claude-3-haiku-20240307" ] ,
7235+ deepseek : [ "deepseek-chat" , "deepseek-reasoner" ] ,
7236+ kimi : [ "kimi-k2-0520" , "moonshot-v1-128k" , "moonshot-v1-32k" ] ,
7237+ } ;
7238+
7239+ try {
7240+ let models = [ ] ;
7241+
7242+ if ( provider === "gemini" || provider === "google" ) {
7243+ const geminiUrl = ( base_url || "https://generativelanguage.googleapis.com" ) + "/v1beta/models?key=" + api_key ;
7244+ const res = await fetch ( geminiUrl ) ;
7245+ if ( ! res . ok ) {
7246+ const errText = await res . text ( ) . catch ( ( ) => "" ) ;
7247+ console . error ( `[BYOK-Validate] Gemini FAILED: ${ res . status } ${ errText . substring ( 0 , 200 ) } ` ) ;
7248+ return apiResponse ( {
7249+ connected : false , provider : "gemini" ,
7250+ error : res . status === 400 || res . status === 403 ? "Invalid API key" : `API error ${ res . status } : ${ errText . substring ( 0 , 100 ) } ` ,
7251+ } ) ;
7252+ }
7253+ const data = await res . json ( ) ;
7254+ models = ( data . models || [ ] )
7255+ . filter ( m => m . supportedGenerationMethods && m . supportedGenerationMethods . includes ( "generateContent" ) )
7256+ . map ( m => {
7257+ const mid = m . name ? m . name . replace ( "models/" , "" ) : m . name ;
7258+ return {
7259+ id : mid , name : m . displayName || mid ,
7260+ context_window : m . inputTokenLimit || null ,
7261+ output_limit : m . outputTokenLimit || null ,
7262+ recommended : ( RECOMMENDED . gemini || [ ] ) . includes ( mid ) ,
7263+ } ;
7264+ } )
7265+ . sort ( ( a , b ) => ( b . recommended ? 1 : 0 ) - ( a . recommended ? 1 : 0 ) ) ;
7266+
7267+ } else if ( provider === "openai" ) {
7268+ const oaiUrl = ( base_url || "https://api.openai.com/v1" ) + "/models" ;
7269+ const res = await fetch ( oaiUrl , { headers : { Authorization : `Bearer ${ api_key } ` } } ) ;
7270+ if ( ! res . ok ) {
7271+ const errText = await res . text ( ) . catch ( ( ) => "" ) ;
7272+ return apiResponse ( {
7273+ connected : false , provider : "openai" ,
7274+ error : res . status === 401 ? "Invalid API key" : `API error ${ res . status } : ${ errText . substring ( 0 , 100 ) } ` ,
7275+ } ) ;
7276+ }
7277+ const data = await res . json ( ) ;
7278+ models = ( data . data || [ ] )
7279+ . filter ( m => m . id && ( m . id . includes ( "gpt" ) || m . id . includes ( "o1" ) || m . id . includes ( "o3" ) || m . id . includes ( "o4" ) || m . id . includes ( "chatgpt" ) ) )
7280+ . map ( m => ( { id : m . id , name : m . id , recommended : ( RECOMMENDED . openai || [ ] ) . includes ( m . id ) } ) )
7281+ . sort ( ( a , b ) => ( b . recommended ? 1 : 0 ) - ( a . recommended ? 1 : 0 ) ) ;
7282+
7283+ } else if ( provider === "anthropic" ) {
7284+ const antUrl = ( base_url || "https://api.anthropic.com/v1" ) + "/messages" ;
7285+ const res = await fetch ( antUrl , {
7286+ method : "POST" ,
7287+ headers : { "Content-Type" : "application/json" , "x-api-key" : api_key , "anthropic-version" : "2023-06-01" } ,
7288+ body : JSON . stringify ( { model : "claude-3-haiku-20240307" , max_tokens : 1 , messages : [ { role : "user" , content : "hi" } ] } ) ,
7289+ } ) ;
7290+ if ( ! res . ok ) {
7291+ const errText = await res . text ( ) . catch ( ( ) => "" ) ;
7292+ if ( res . status === 401 || errText . includes ( "invalid x-api-key" ) ) {
7293+ return apiResponse ( { connected : false , provider : "anthropic" , error : "Invalid API key" } ) ;
7294+ }
7295+ }
7296+ models = [
7297+ { id : "claude-sonnet-4-20250514" , name : "Claude Sonnet 4 (Latest)" , recommended : true } ,
7298+ { id : "claude-3-7-sonnet-20250219" , name : "Claude 3.7 Sonnet" , recommended : true } ,
7299+ { id : "claude-3-5-sonnet-20241022" , name : "Claude 3.5 Sonnet" , recommended : true } ,
7300+ { id : "claude-3-haiku-20240307" , name : "Claude 3 Haiku (Fast)" , recommended : true } ,
7301+ { id : "claude-3-opus-20240229" , name : "Claude 3 Opus (Powerful)" , recommended : false } ,
7302+ ] ;
7303+
7304+ } else if ( provider === "deepseek" ) {
7305+ const dsUrl = ( base_url || "https://api.deepseek.com/v1" ) + "/models" ;
7306+ const res = await fetch ( dsUrl , { headers : { Authorization : `Bearer ${ api_key } ` } } ) ;
7307+ if ( ! res . ok ) {
7308+ const errText = await res . text ( ) . catch ( ( ) => "" ) ;
7309+ return apiResponse ( {
7310+ connected : false , provider : "deepseek" ,
7311+ error : res . status === 401 ? "Invalid API key" : `API error ${ res . status } : ${ errText . substring ( 0 , 100 ) } ` ,
7312+ } ) ;
7313+ }
7314+ const data = await res . json ( ) ;
7315+ models = ( data . data || [ ] ) . map ( m => ( {
7316+ id : m . id , name : m . id , recommended : ( RECOMMENDED . deepseek || [ ] ) . includes ( m . id ) ,
7317+ } ) ) ;
7318+
7319+ } else if ( provider === "kimi" ) {
7320+ const kimiUrl = ( base_url || "https://api.moonshot.cn/v1" ) + "/models" ;
7321+ const res = await fetch ( kimiUrl , { headers : { Authorization : `Bearer ${ api_key } ` } } ) ;
7322+ if ( ! res . ok ) {
7323+ const errText = await res . text ( ) . catch ( ( ) => "" ) ;
7324+ return apiResponse ( {
7325+ connected : false , provider : "kimi" ,
7326+ error : res . status === 401 ? "Invalid API key" : `API error ${ res . status } : ${ errText . substring ( 0 , 100 ) } ` ,
7327+ } ) ;
7328+ }
7329+ const data = await res . json ( ) ;
7330+ models = ( data . data || [ ] ) . map ( m => ( {
7331+ id : m . id , name : m . id , recommended : ( RECOMMENDED . kimi || [ ] ) . includes ( m . id ) ,
7332+ } ) ) ;
7333+
7334+ } else {
7335+ const customUrl = ( base_url || "" ) . replace ( / \/ $ / , "" ) + "/models" ;
7336+ try {
7337+ const res = await fetch ( customUrl , { headers : { Authorization : `Bearer ${ api_key } ` } } ) ;
7338+ if ( res . ok ) {
7339+ const data = await res . json ( ) ;
7340+ models = ( data . data || [ ] ) . map ( m => ( { id : m . id || m . name , name : m . id || m . name , recommended : false } ) ) ;
7341+ }
7342+ } catch ( e ) {
7343+ console . warn ( `[BYOK-Validate] Custom model listing failed: ${ e . message } ` ) ;
7344+ }
7345+ if ( models . length === 0 ) {
7346+ return apiResponse ( { connected : true , provider, models : [ ] , message : "Connected but model listing not supported. Enter model name manually." } ) ;
7347+ }
7348+ }
7349+
7350+ console . log ( `[BYOK-Validate] SUCCESS: provider=${ provider } , models_found=${ models . length } ` ) ;
7351+ return apiResponse ( { connected : true , provider, models, total : models . length } ) ;
7352+
7353+ } catch ( err ) {
7354+ console . error ( `[BYOK-Validate] Error: ${ err . message } ` ) ;
7355+ return apiResponse ( { connected : false , provider, error : `Connection failed: ${ err . message } ` } ) ;
7356+ }
7357+ }
0 commit comments