@@ -49,6 +49,15 @@ type OAuthStatus =
4949 opusModel : string
5050 activeField : 'base_url' | 'api_key' | 'haiku_model' | 'sonnet_model' | 'opus_model'
5151 } // OpenAI Chat Completions API platform
52+ | {
53+ state : 'gemini_api'
54+ baseUrl : string
55+ apiKey : string
56+ haikuModel : string
57+ sonnetModel : string
58+ opusModel : string
59+ activeField : 'base_url' | 'api_key' | 'haiku_model' | 'sonnet_model' | 'opus_model'
60+ } // Gemini Generate Content API platform
5261 | { state : 'ready_to_start' } // Flow started, waiting for browser to open
5362 | { state : 'waiting_for_login' ; url : string } // Browser opened, waiting for user to login
5463 | { state : 'creating_api_key' } // Got access token, creating API key
@@ -61,7 +70,6 @@ type OAuthStatus =
6170 }
6271
6372const PASTE_HERE_MSG = 'Paste code here if prompted > '
64-
6573export function ConsoleOAuthFlow ( {
6674 onDone,
6775 startingMessage,
@@ -470,6 +478,16 @@ function OAuthStatusMessage({
470478 ) ,
471479 value : 'openai_chat_api' ,
472480 } ,
481+ {
482+ label : (
483+ < Text >
484+ Gemini API ·{ ' ' }
485+ < Text dimColor > Google Gemini native REST/SSE</ Text >
486+ { '\n' }
487+ </ Text >
488+ ) ,
489+ value : 'gemini_api' ,
490+ } ,
473491 {
474492 label : (
475493 < Text >
@@ -537,6 +555,17 @@ function OAuthStatusMessage({
537555 opusModel : process . env . OPENAI_DEFAULT_OPUS_MODEL ?? '' ,
538556 activeField : 'base_url' ,
539557 } )
558+ } else if ( value === 'gemini_api' ) {
559+ logEvent ( 'tengu_gemini_api_selected' , { } )
560+ setOAuthStatus ( {
561+ state : 'gemini_api' ,
562+ baseUrl : process . env . GEMINI_BASE_URL ?? '' ,
563+ apiKey : process . env . GEMINI_API_KEY ?? '' ,
564+ haikuModel : process . env . ANTHROPIC_DEFAULT_HAIKU_MODEL ?? '' ,
565+ sonnetModel : process . env . ANTHROPIC_DEFAULT_SONNET_MODEL ?? '' ,
566+ opusModel : process . env . ANTHROPIC_DEFAULT_OPUS_MODEL ?? '' ,
567+ activeField : 'base_url' ,
568+ } )
540569 } else if ( value === 'platform' ) {
541570 logEvent ( 'tengu_oauth_platform_selected' , { } )
542571 setOAuthStatus ( { state : 'platform_setup' } )
@@ -1014,6 +1043,238 @@ function OAuthStatusMessage({
10141043 )
10151044 }
10161045
1046+ case 'gemini_api' :
1047+ {
1048+ type GeminiField = 'base_url' | 'api_key' | 'haiku_model' | 'sonnet_model' | 'opus_model'
1049+ const GEMINI_FIELDS : GeminiField [ ] = [
1050+ 'base_url' ,
1051+ 'api_key' ,
1052+ 'haiku_model' ,
1053+ 'sonnet_model' ,
1054+ 'opus_model' ,
1055+ ]
1056+ const gp = oauthStatus as {
1057+ state : 'gemini_api'
1058+ activeField : GeminiField
1059+ baseUrl : string
1060+ apiKey : string
1061+ haikuModel : string
1062+ sonnetModel : string
1063+ opusModel : string
1064+ }
1065+ const { activeField, baseUrl, apiKey, haikuModel, sonnetModel, opusModel } = gp
1066+ const geminiDisplayValues : Record < GeminiField , string > = {
1067+ base_url : baseUrl ,
1068+ api_key : apiKey ,
1069+ haiku_model : haikuModel ,
1070+ sonnet_model : sonnetModel ,
1071+ opus_model : opusModel ,
1072+ }
1073+
1074+ const [ geminiInputValue , setGeminiInputValue ] = useState (
1075+ ( ) => geminiDisplayValues [ activeField ] ,
1076+ )
1077+ const [ geminiInputCursorOffset , setGeminiInputCursorOffset ] = useState (
1078+ ( ) => geminiDisplayValues [ activeField ] . length ,
1079+ )
1080+
1081+ const buildGeminiState = useCallback (
1082+ ( field : GeminiField , value : string , newActive ?: GeminiField ) => {
1083+ const s = {
1084+ state : 'gemini_api' as const ,
1085+ activeField : newActive ?? activeField ,
1086+ baseUrl,
1087+ apiKey,
1088+ haikuModel,
1089+ sonnetModel,
1090+ opusModel,
1091+ }
1092+ switch ( field ) {
1093+ case 'base_url' :
1094+ return { ...s , baseUrl : value }
1095+ case 'api_key' :
1096+ return { ...s , apiKey : value }
1097+ case 'haiku_model' :
1098+ return { ...s , haikuModel : value }
1099+ case 'sonnet_model' :
1100+ return { ...s , sonnetModel : value }
1101+ case 'opus_model' :
1102+ return { ...s , opusModel : value }
1103+ }
1104+ } ,
1105+ [ activeField , baseUrl , apiKey , haikuModel , sonnetModel , opusModel ] ,
1106+ )
1107+
1108+ const doGeminiSave = useCallback ( ( ) => {
1109+ const finalVals = { ...geminiDisplayValues , [ activeField ] : geminiInputValue }
1110+ if ( ! finalVals . haiku_model || ! finalVals . sonnet_model || ! finalVals . opus_model ) {
1111+ setOAuthStatus ( {
1112+ state : 'error' ,
1113+ message : 'Gemini setup requires Haiku, Sonnet, and Opus model names.' ,
1114+ toRetry : {
1115+ state : 'gemini_api' ,
1116+ baseUrl : finalVals . base_url ,
1117+ apiKey : finalVals . api_key ,
1118+ haikuModel : finalVals . haiku_model ,
1119+ sonnetModel : finalVals . sonnet_model ,
1120+ opusModel : finalVals . opus_model ,
1121+ activeField,
1122+ } ,
1123+ } )
1124+ return
1125+ }
1126+
1127+ const env : Record < string , string > = { }
1128+ if ( finalVals . base_url ) env . GEMINI_BASE_URL = finalVals . base_url
1129+ if ( finalVals . api_key ) env . GEMINI_API_KEY = finalVals . api_key
1130+ if ( finalVals . haiku_model ) env . ANTHROPIC_DEFAULT_HAIKU_MODEL = finalVals . haiku_model
1131+ if ( finalVals . sonnet_model ) env . ANTHROPIC_DEFAULT_SONNET_MODEL = finalVals . sonnet_model
1132+ if ( finalVals . opus_model ) env . ANTHROPIC_DEFAULT_OPUS_MODEL = finalVals . opus_model
1133+ const { error } = updateSettingsForSource ( 'userSettings' , {
1134+ modelType : 'gemini' as any ,
1135+ env,
1136+ } as any )
1137+ if ( error ) {
1138+ setOAuthStatus ( {
1139+ state : 'error' ,
1140+ message : `Failed to save: ${ error . message } ` ,
1141+ toRetry : {
1142+ state : 'gemini_api' ,
1143+ baseUrl : '' ,
1144+ apiKey : '' ,
1145+ haikuModel : '' ,
1146+ sonnetModel : '' ,
1147+ opusModel : '' ,
1148+ activeField : 'base_url' ,
1149+ } ,
1150+ } )
1151+ } else {
1152+ for ( const [ k , v ] of Object . entries ( env ) ) process . env [ k ] = v
1153+ setOAuthStatus ( { state : 'success' } )
1154+ void onDone ( )
1155+ }
1156+ } , [ activeField , geminiInputValue , geminiDisplayValues , onDone , setOAuthStatus ] )
1157+
1158+ const handleGeminiEnter = useCallback ( ( ) => {
1159+ const idx = GEMINI_FIELDS . indexOf ( activeField )
1160+ setOAuthStatus ( buildGeminiState ( activeField , geminiInputValue ) )
1161+ if ( idx === GEMINI_FIELDS . length - 1 ) {
1162+ doGeminiSave ( )
1163+ } else {
1164+ const next = GEMINI_FIELDS [ idx + 1 ] !
1165+ setGeminiInputValue ( geminiDisplayValues [ next ] ?? '' )
1166+ setGeminiInputCursorOffset ( ( geminiDisplayValues [ next ] ?? '' ) . length )
1167+ }
1168+ } , [
1169+ activeField ,
1170+ buildGeminiState ,
1171+ doGeminiSave ,
1172+ geminiDisplayValues ,
1173+ geminiInputValue ,
1174+ setOAuthStatus ,
1175+ ] )
1176+
1177+ useKeybinding (
1178+ 'tabs:next' ,
1179+ ( ) => {
1180+ const idx = GEMINI_FIELDS . indexOf ( activeField )
1181+ if ( idx < GEMINI_FIELDS . length - 1 ) {
1182+ setOAuthStatus (
1183+ buildGeminiState ( activeField , geminiInputValue , GEMINI_FIELDS [ idx + 1 ] ) ,
1184+ )
1185+ setGeminiInputValue ( geminiDisplayValues [ GEMINI_FIELDS [ idx + 1 ] ! ] ?? '' )
1186+ setGeminiInputCursorOffset (
1187+ ( geminiDisplayValues [ GEMINI_FIELDS [ idx + 1 ] ! ] ?? '' ) . length ,
1188+ )
1189+ }
1190+ } ,
1191+ { context : 'Tabs' } ,
1192+ )
1193+ useKeybinding (
1194+ 'tabs:previous' ,
1195+ ( ) => {
1196+ const idx = GEMINI_FIELDS . indexOf ( activeField )
1197+ if ( idx > 0 ) {
1198+ setOAuthStatus (
1199+ buildGeminiState ( activeField , geminiInputValue , GEMINI_FIELDS [ idx - 1 ] ) ,
1200+ )
1201+ setGeminiInputValue ( geminiDisplayValues [ GEMINI_FIELDS [ idx - 1 ] ! ] ?? '' )
1202+ setGeminiInputCursorOffset (
1203+ ( geminiDisplayValues [ GEMINI_FIELDS [ idx - 1 ] ! ] ?? '' ) . length ,
1204+ )
1205+ }
1206+ } ,
1207+ { context : 'Tabs' } ,
1208+ )
1209+ useKeybinding (
1210+ 'confirm:no' ,
1211+ ( ) => {
1212+ setOAuthStatus ( { state : 'idle' } )
1213+ } ,
1214+ { context : 'Confirmation' } ,
1215+ )
1216+
1217+ const geminiColumns = useTerminalSize ( ) . columns - 20
1218+
1219+ const renderGeminiRow = (
1220+ field : GeminiField ,
1221+ label : string ,
1222+ opts ?: { mask ?: boolean } ,
1223+ ) => {
1224+ const active = activeField === field
1225+ const val = geminiDisplayValues [ field ]
1226+ return (
1227+ < Box >
1228+ < Text
1229+ backgroundColor = { active ? 'suggestion' : undefined }
1230+ color = { active ? 'inverseText' : undefined }
1231+ >
1232+ { ` ${ label } ` }
1233+ </ Text >
1234+ < Text > </ Text >
1235+ { active ? (
1236+ < TextInput
1237+ value = { geminiInputValue }
1238+ onChange = { setGeminiInputValue }
1239+ onSubmit = { handleGeminiEnter }
1240+ cursorOffset = { geminiInputCursorOffset }
1241+ onChangeCursorOffset = { setGeminiInputCursorOffset }
1242+ columns = { geminiColumns }
1243+ mask = { opts ?. mask ? '*' : undefined }
1244+ focus = { true }
1245+ />
1246+ ) : val ? (
1247+ < Text color = "success" >
1248+ { opts ?. mask
1249+ ? val . slice ( 0 , 8 ) + '\u00b7' . repeat ( Math . max ( 0 , val . length - 8 ) )
1250+ : val }
1251+ </ Text >
1252+ ) : null }
1253+ </ Box >
1254+ )
1255+ }
1256+
1257+ return (
1258+ < Box flexDirection = "column" gap = { 1 } >
1259+ < Text bold > Gemini API Setup</ Text >
1260+ < Text dimColor >
1261+ Configure a Gemini Generate Content compatible endpoint. Base URL is
1262+ optional and defaults to Google's v1beta API.
1263+ </ Text >
1264+ < Box flexDirection = "column" gap = { 1 } >
1265+ { renderGeminiRow ( 'base_url' , 'Base URL ' ) }
1266+ { renderGeminiRow ( 'api_key' , 'API Key ' , { mask : true } ) }
1267+ { renderGeminiRow ( 'haiku_model' , 'Haiku ' ) }
1268+ { renderGeminiRow ( 'sonnet_model' , 'Sonnet ' ) }
1269+ { renderGeminiRow ( 'opus_model' , 'Opus ' ) }
1270+ </ Box >
1271+ < Text dimColor >
1272+ Tab to switch · Enter on last field to save · Esc to go back
1273+ </ Text >
1274+ </ Box >
1275+ )
1276+ }
1277+
10171278 case 'platform_setup' :
10181279 return (
10191280 < Box flexDirection = "column" gap = { 1 } marginTop = { 1 } >
0 commit comments