@@ -48,6 +48,15 @@ type OAuthStatus =
4848 opusModel : string
4949 activeField : 'base_url' | 'api_key' | 'haiku_model' | 'sonnet_model' | 'opus_model'
5050 } // OpenAI Chat Completions API platform
51+ | {
52+ state : 'gemini_api'
53+ baseUrl : string
54+ apiKey : string
55+ haikuModel : string
56+ sonnetModel : string
57+ opusModel : string
58+ activeField : 'base_url' | 'api_key' | 'haiku_model' | 'sonnet_model' | 'opus_model'
59+ } // Gemini Generate Content API platform
5160 | { state : 'ready_to_start' } // Flow started, waiting for browser to open
5261 | { state : 'waiting_for_login' ; url : string } // Browser opened, waiting for user to login
5362 | { state : 'creating_api_key' } // Got access token, creating API key
@@ -60,7 +69,6 @@ type OAuthStatus =
6069 }
6170
6271const PASTE_HERE_MSG = 'Paste code here if prompted > '
63-
6472export function ConsoleOAuthFlow ( {
6573 onDone,
6674 startingMessage,
@@ -476,6 +484,16 @@ function OAuthStatusMessage({
476484 ) ,
477485 value : 'openai_chat_api' ,
478486 } ,
487+ {
488+ label : (
489+ < Text >
490+ Gemini API ·{ ' ' }
491+ < Text dimColor > Google Gemini native REST/SSE</ Text >
492+ { '\n' }
493+ </ Text >
494+ ) ,
495+ value : 'gemini_api' ,
496+ } ,
479497 {
480498 label : (
481499 < Text >
@@ -543,6 +561,17 @@ function OAuthStatusMessage({
543561 opusModel : process . env . ANTHROPIC_DEFAULT_OPUS_MODEL ?? '' ,
544562 activeField : 'base_url' ,
545563 } )
564+ } else if ( value === 'gemini_api' ) {
565+ logEvent ( 'tengu_gemini_api_selected' , { } )
566+ setOAuthStatus ( {
567+ state : 'gemini_api' ,
568+ baseUrl : process . env . GEMINI_BASE_URL ?? '' ,
569+ apiKey : process . env . GEMINI_API_KEY ?? '' ,
570+ haikuModel : process . env . ANTHROPIC_DEFAULT_HAIKU_MODEL ?? '' ,
571+ sonnetModel : process . env . ANTHROPIC_DEFAULT_SONNET_MODEL ?? '' ,
572+ opusModel : process . env . ANTHROPIC_DEFAULT_OPUS_MODEL ?? '' ,
573+ activeField : 'base_url' ,
574+ } )
546575 } else if ( value === 'platform' ) {
547576 logEvent ( 'tengu_oauth_platform_selected' , { } )
548577 setOAuthStatus ( { state : 'platform_setup' } )
@@ -974,6 +1003,238 @@ function OAuthStatusMessage({
9741003 )
9751004 }
9761005
1006+ case 'gemini_api' :
1007+ {
1008+ type GeminiField = 'base_url' | 'api_key' | 'haiku_model' | 'sonnet_model' | 'opus_model'
1009+ const GEMINI_FIELDS : GeminiField [ ] = [
1010+ 'base_url' ,
1011+ 'api_key' ,
1012+ 'haiku_model' ,
1013+ 'sonnet_model' ,
1014+ 'opus_model' ,
1015+ ]
1016+ const gp = oauthStatus as {
1017+ state : 'gemini_api'
1018+ activeField : GeminiField
1019+ baseUrl : string
1020+ apiKey : string
1021+ haikuModel : string
1022+ sonnetModel : string
1023+ opusModel : string
1024+ }
1025+ const { activeField, baseUrl, apiKey, haikuModel, sonnetModel, opusModel } = gp
1026+ const geminiDisplayValues : Record < GeminiField , string > = {
1027+ base_url : baseUrl ,
1028+ api_key : apiKey ,
1029+ haiku_model : haikuModel ,
1030+ sonnet_model : sonnetModel ,
1031+ opus_model : opusModel ,
1032+ }
1033+
1034+ const [ geminiInputValue , setGeminiInputValue ] = useState (
1035+ ( ) => geminiDisplayValues [ activeField ] ,
1036+ )
1037+ const [ geminiInputCursorOffset , setGeminiInputCursorOffset ] = useState (
1038+ ( ) => geminiDisplayValues [ activeField ] . length ,
1039+ )
1040+
1041+ const buildGeminiState = useCallback (
1042+ ( field : GeminiField , value : string , newActive ?: GeminiField ) => {
1043+ const s = {
1044+ state : 'gemini_api' as const ,
1045+ activeField : newActive ?? activeField ,
1046+ baseUrl,
1047+ apiKey,
1048+ haikuModel,
1049+ sonnetModel,
1050+ opusModel,
1051+ }
1052+ switch ( field ) {
1053+ case 'base_url' :
1054+ return { ...s , baseUrl : value }
1055+ case 'api_key' :
1056+ return { ...s , apiKey : value }
1057+ case 'haiku_model' :
1058+ return { ...s , haikuModel : value }
1059+ case 'sonnet_model' :
1060+ return { ...s , sonnetModel : value }
1061+ case 'opus_model' :
1062+ return { ...s , opusModel : value }
1063+ }
1064+ } ,
1065+ [ activeField , baseUrl , apiKey , haikuModel , sonnetModel , opusModel ] ,
1066+ )
1067+
1068+ const doGeminiSave = useCallback ( ( ) => {
1069+ const finalVals = { ...geminiDisplayValues , [ activeField ] : geminiInputValue }
1070+ if ( ! finalVals . haiku_model || ! finalVals . sonnet_model || ! finalVals . opus_model ) {
1071+ setOAuthStatus ( {
1072+ state : 'error' ,
1073+ message : 'Gemini setup requires Haiku, Sonnet, and Opus model names.' ,
1074+ toRetry : {
1075+ state : 'gemini_api' ,
1076+ baseUrl : finalVals . base_url ,
1077+ apiKey : finalVals . api_key ,
1078+ haikuModel : finalVals . haiku_model ,
1079+ sonnetModel : finalVals . sonnet_model ,
1080+ opusModel : finalVals . opus_model ,
1081+ activeField,
1082+ } ,
1083+ } )
1084+ return
1085+ }
1086+
1087+ const env : Record < string , string > = { }
1088+ if ( finalVals . base_url ) env . GEMINI_BASE_URL = finalVals . base_url
1089+ if ( finalVals . api_key ) env . GEMINI_API_KEY = finalVals . api_key
1090+ if ( finalVals . haiku_model ) env . ANTHROPIC_DEFAULT_HAIKU_MODEL = finalVals . haiku_model
1091+ if ( finalVals . sonnet_model ) env . ANTHROPIC_DEFAULT_SONNET_MODEL = finalVals . sonnet_model
1092+ if ( finalVals . opus_model ) env . ANTHROPIC_DEFAULT_OPUS_MODEL = finalVals . opus_model
1093+ const { error } = updateSettingsForSource ( 'userSettings' , {
1094+ modelType : 'gemini' as any ,
1095+ env,
1096+ } as any )
1097+ if ( error ) {
1098+ setOAuthStatus ( {
1099+ state : 'error' ,
1100+ message : `Failed to save: ${ error . message } ` ,
1101+ toRetry : {
1102+ state : 'gemini_api' ,
1103+ baseUrl : '' ,
1104+ apiKey : '' ,
1105+ haikuModel : '' ,
1106+ sonnetModel : '' ,
1107+ opusModel : '' ,
1108+ activeField : 'base_url' ,
1109+ } ,
1110+ } )
1111+ } else {
1112+ for ( const [ k , v ] of Object . entries ( env ) ) process . env [ k ] = v
1113+ setOAuthStatus ( { state : 'success' } )
1114+ void onDone ( )
1115+ }
1116+ } , [ activeField , geminiInputValue , geminiDisplayValues , onDone , setOAuthStatus ] )
1117+
1118+ const handleGeminiEnter = useCallback ( ( ) => {
1119+ const idx = GEMINI_FIELDS . indexOf ( activeField )
1120+ setOAuthStatus ( buildGeminiState ( activeField , geminiInputValue ) )
1121+ if ( idx === GEMINI_FIELDS . length - 1 ) {
1122+ doGeminiSave ( )
1123+ } else {
1124+ const next = GEMINI_FIELDS [ idx + 1 ] !
1125+ setGeminiInputValue ( geminiDisplayValues [ next ] ?? '' )
1126+ setGeminiInputCursorOffset ( ( geminiDisplayValues [ next ] ?? '' ) . length )
1127+ }
1128+ } , [
1129+ activeField ,
1130+ buildGeminiState ,
1131+ doGeminiSave ,
1132+ geminiDisplayValues ,
1133+ geminiInputValue ,
1134+ setOAuthStatus ,
1135+ ] )
1136+
1137+ useKeybinding (
1138+ 'tabs:next' ,
1139+ ( ) => {
1140+ const idx = GEMINI_FIELDS . indexOf ( activeField )
1141+ if ( idx < GEMINI_FIELDS . length - 1 ) {
1142+ setOAuthStatus (
1143+ buildGeminiState ( activeField , geminiInputValue , GEMINI_FIELDS [ idx + 1 ] ) ,
1144+ )
1145+ setGeminiInputValue ( geminiDisplayValues [ GEMINI_FIELDS [ idx + 1 ] ! ] ?? '' )
1146+ setGeminiInputCursorOffset (
1147+ ( geminiDisplayValues [ GEMINI_FIELDS [ idx + 1 ] ! ] ?? '' ) . length ,
1148+ )
1149+ }
1150+ } ,
1151+ { context : 'Tabs' } ,
1152+ )
1153+ useKeybinding (
1154+ 'tabs:previous' ,
1155+ ( ) => {
1156+ const idx = GEMINI_FIELDS . indexOf ( activeField )
1157+ if ( idx > 0 ) {
1158+ setOAuthStatus (
1159+ buildGeminiState ( activeField , geminiInputValue , GEMINI_FIELDS [ idx - 1 ] ) ,
1160+ )
1161+ setGeminiInputValue ( geminiDisplayValues [ GEMINI_FIELDS [ idx - 1 ] ! ] ?? '' )
1162+ setGeminiInputCursorOffset (
1163+ ( geminiDisplayValues [ GEMINI_FIELDS [ idx - 1 ] ! ] ?? '' ) . length ,
1164+ )
1165+ }
1166+ } ,
1167+ { context : 'Tabs' } ,
1168+ )
1169+ useKeybinding (
1170+ 'confirm:no' ,
1171+ ( ) => {
1172+ setOAuthStatus ( { state : 'idle' } )
1173+ } ,
1174+ { context : 'Confirmation' } ,
1175+ )
1176+
1177+ const geminiColumns = useTerminalSize ( ) . columns - 20
1178+
1179+ const renderGeminiRow = (
1180+ field : GeminiField ,
1181+ label : string ,
1182+ opts ?: { mask ?: boolean } ,
1183+ ) => {
1184+ const active = activeField === field
1185+ const val = geminiDisplayValues [ field ]
1186+ return (
1187+ < Box >
1188+ < Text
1189+ backgroundColor = { active ? 'suggestion' : undefined }
1190+ color = { active ? 'inverseText' : undefined }
1191+ >
1192+ { ` ${ label } ` }
1193+ </ Text >
1194+ < Text > </ Text >
1195+ { active ? (
1196+ < TextInput
1197+ value = { geminiInputValue }
1198+ onChange = { setGeminiInputValue }
1199+ onSubmit = { handleGeminiEnter }
1200+ cursorOffset = { geminiInputCursorOffset }
1201+ onChangeCursorOffset = { setGeminiInputCursorOffset }
1202+ columns = { geminiColumns }
1203+ mask = { opts ?. mask ? '*' : undefined }
1204+ focus = { true }
1205+ />
1206+ ) : val ? (
1207+ < Text color = "success" >
1208+ { opts ?. mask
1209+ ? val . slice ( 0 , 8 ) + '\u00b7' . repeat ( Math . max ( 0 , val . length - 8 ) )
1210+ : val }
1211+ </ Text >
1212+ ) : null }
1213+ </ Box >
1214+ )
1215+ }
1216+
1217+ return (
1218+ < Box flexDirection = "column" gap = { 1 } >
1219+ < Text bold > Gemini API Setup</ Text >
1220+ < Text dimColor >
1221+ Configure a Gemini Generate Content compatible endpoint. Base URL is
1222+ optional and defaults to Google's v1beta API.
1223+ </ Text >
1224+ < Box flexDirection = "column" gap = { 1 } >
1225+ { renderGeminiRow ( 'base_url' , 'Base URL ' ) }
1226+ { renderGeminiRow ( 'api_key' , 'API Key ' , { mask : true } ) }
1227+ { renderGeminiRow ( 'haiku_model' , 'Haiku ' ) }
1228+ { renderGeminiRow ( 'sonnet_model' , 'Sonnet ' ) }
1229+ { renderGeminiRow ( 'opus_model' , 'Opus ' ) }
1230+ </ Box >
1231+ < Text dimColor >
1232+ Tab to switch · Enter on last field to save · Esc to go back
1233+ </ Text >
1234+ </ Box >
1235+ )
1236+ }
1237+
9771238 case 'platform_setup' :
9781239 return (
9791240 < Box flexDirection = "column" gap = { 1 } marginTop = { 1 } >
0 commit comments