@@ -38,6 +38,15 @@ type OAuthStatus = {
3838 opusModel : string ;
3939 activeField : 'base_url' | 'api_key' | 'haiku_model' | 'sonnet_model' | 'opus_model' ;
4040} // Custom platform: configure API endpoint and model names
41+ | {
42+ state : 'openai_chat_api' ;
43+ baseUrl : string ;
44+ apiKey : string ;
45+ haikuModel : string ;
46+ sonnetModel : string ;
47+ opusModel : string ;
48+ activeField : 'base_url' | 'api_key' | 'haiku_model' | 'sonnet_model' | 'opus_model' ;
49+ } // OpenAI Chat Completions API platform
4150| {
4251 state : 'ready_to_start' ;
4352} // Flow started, waiting for browser to open
@@ -246,6 +255,8 @@ export function ConsoleOAuthFlow({
246255 if ( ! orgResult . valid ) {
247256 throw new Error ( ( orgResult as { valid : false ; message : string } ) . message ) ;
248257 }
258+ // Reset modelType to anthropic when using OAuth login
259+ updateSettingsForSource ( 'userSettings' , { modelType : 'anthropic' } as any ) ;
249260 setOAuthStatus ( {
250261 state : 'success'
251262 } ) ;
@@ -416,6 +427,9 @@ function OAuthStatusMessage(t0) {
416427 t6 = [ {
417428 label : < Text > Custom Platform ·{ " " } < Text dimColor = { true } > Configure your own API endpoint</ Text > { "\n" } </ Text > ,
418429 value : "custom_platform"
430+ } , {
431+ label : < Text > OpenAI Compatible ·{ " " } < Text dimColor = { true } > Ollama, DeepSeek, vLLM, One API, etc.</ Text > { "\n" } </ Text > ,
432+ value : "openai_chat_api"
419433 } , t4 , t5 , {
420434 label : < Text > 3rd-party platform ·{ " " } < Text dimColor = { true } > Amazon Bedrock, Microsoft Foundry, or Vertex AI</ Text > { "\n" } </ Text > ,
421435 value : "platform"
@@ -438,6 +452,17 @@ function OAuthStatusMessage(t0) {
438452 opusModel : process . env . ANTHROPIC_DEFAULT_OPUS_MODEL ?? "" ,
439453 activeField : "base_url"
440454 } ) ;
455+ } else if ( value_0 === "openai_chat_api" ) {
456+ logEvent ( "tengu_openai_chat_api_selected" , { } ) ;
457+ setOAuthStatus ( {
458+ state : "openai_chat_api" ,
459+ baseUrl : process . env . OPENAI_BASE_URL ?? "" ,
460+ apiKey : process . env . OPENAI_API_KEY ?? "" ,
461+ haikuModel : process . env . ANTHROPIC_DEFAULT_HAIKU_MODEL ?? "" ,
462+ sonnetModel : process . env . ANTHROPIC_DEFAULT_SONNET_MODEL ?? "" ,
463+ opusModel : process . env . ANTHROPIC_DEFAULT_OPUS_MODEL ?? "" ,
464+ activeField : "base_url"
465+ } ) ;
441466 } else if ( value_0 === "platform" ) {
442467 logEvent ( "tengu_oauth_platform_selected" , { } ) ;
443468 setOAuthStatus ( {
@@ -568,7 +593,7 @@ function OAuthStatusMessage(t0) {
568593 if ( finalVals . haiku_model ) env . ANTHROPIC_DEFAULT_HAIKU_MODEL = finalVals . haiku_model ;
569594 if ( finalVals . sonnet_model ) env . ANTHROPIC_DEFAULT_SONNET_MODEL = finalVals . sonnet_model ;
570595 if ( finalVals . opus_model ) env . ANTHROPIC_DEFAULT_OPUS_MODEL = finalVals . opus_model ;
571- const { error } = updateSettingsForSource ( 'userSettings' , { env } as any ) ;
596+ const { error } = updateSettingsForSource ( 'userSettings' , { modelType : 'anthropic' as any , env } as any ) ;
572597 if ( error ) {
573598 setOAuthStatus ( { state : 'error' , message : `Failed to save: ${ error . message } ` , toRetry : { state : 'custom_platform' , baseUrl : '' , apiKey : '' , haikuModel : '' , sonnetModel : '' , opusModel : '' , activeField : 'base_url' } } ) ;
574599 } else {
@@ -639,6 +664,107 @@ function OAuthStatusMessage(t0) {
639664 < Text dimColor > Tab to switch · Enter on last field to save · Esc to go back</ Text >
640665 </ Box > ;
641666 }
667+ case "openai_chat_api" :
668+ {
669+ type OpenAIField = 'base_url' | 'api_key' | 'haiku_model' | 'sonnet_model' | 'opus_model' ;
670+ const OPENAI_FIELDS : OpenAIField [ ] = [ 'base_url' , 'api_key' , 'haiku_model' , 'sonnet_model' , 'opus_model' ] ;
671+ const op = oauthStatus as { state : 'openai_chat_api' ; activeField : OpenAIField ; baseUrl : string ; apiKey : string ; haikuModel : string ; sonnetModel : string ; opusModel : string } ;
672+ const { activeField, baseUrl, apiKey, haikuModel, sonnetModel, opusModel } = op ;
673+ const openaiDisplayValues : Record < OpenAIField , string > = { base_url : baseUrl , api_key : apiKey , haiku_model : haikuModel , sonnet_model : sonnetModel , opus_model : opusModel } ;
674+
675+ const [ openaiInputValue , setOpenaiInputValue ] = useState ( ( ) => openaiDisplayValues [ activeField ] ) ;
676+ const [ openaiInputCursorOffset , setOpenaiInputCursorOffset ] = useState ( ( ) => openaiDisplayValues [ activeField ] . length ) ;
677+
678+ const buildOpenAIState = useCallback ( ( field : OpenAIField , value : string , newActive ?: OpenAIField ) => {
679+ const s = { state : 'openai_chat_api' as const , activeField : newActive ?? activeField , baseUrl, apiKey, haikuModel, sonnetModel, opusModel } ;
680+ switch ( field ) {
681+ case 'base_url' : return { ...s , baseUrl : value } ;
682+ case 'api_key' : return { ...s , apiKey : value } ;
683+ case 'haiku_model' : return { ...s , haikuModel : value } ;
684+ case 'sonnet_model' : return { ...s , sonnetModel : value } ;
685+ case 'opus_model' : return { ...s , opusModel : value } ;
686+ }
687+ } , [ activeField , baseUrl , apiKey , haikuModel , sonnetModel , opusModel ] ) ;
688+
689+ const doOpenAISave = useCallback ( ( ) => {
690+ const finalVals = { ...openaiDisplayValues , [ activeField ] : openaiInputValue } ;
691+ const env : Record < string , string > = { } ;
692+ if ( finalVals . base_url ) env . OPENAI_BASE_URL = finalVals . base_url ;
693+ if ( finalVals . api_key ) env . OPENAI_API_KEY = finalVals . api_key ;
694+ if ( finalVals . haiku_model ) env . ANTHROPIC_DEFAULT_HAIKU_MODEL = finalVals . haiku_model ;
695+ if ( finalVals . sonnet_model ) env . ANTHROPIC_DEFAULT_SONNET_MODEL = finalVals . sonnet_model ;
696+ if ( finalVals . opus_model ) env . ANTHROPIC_DEFAULT_OPUS_MODEL = finalVals . opus_model ;
697+ const { error } = updateSettingsForSource ( 'userSettings' , { modelType : 'openai' as any , env } as any ) ;
698+ if ( error ) {
699+ setOAuthStatus ( { state : 'error' , message : `Failed to save: ${ error . message } ` , toRetry : { state : 'openai_chat_api' , baseUrl : '' , apiKey : '' , haikuModel : '' , sonnetModel : '' , opusModel : '' , activeField : 'base_url' } } ) ;
700+ } else {
701+ for ( const [ k , v ] of Object . entries ( env ) ) process . env [ k ] = v ;
702+ setOAuthStatus ( { state : 'success' } ) ;
703+ void onDone ( ) ;
704+ }
705+ } , [ activeField , openaiInputValue , openaiDisplayValues , setOAuthStatus , onDone ] ) ;
706+
707+ const handleOpenAIEnter = useCallback ( ( ) => {
708+ const idx = OPENAI_FIELDS . indexOf ( activeField ) ;
709+ setOAuthStatus ( buildOpenAIState ( activeField , openaiInputValue ) ) ;
710+ if ( idx === OPENAI_FIELDS . length - 1 ) {
711+ doOpenAISave ( ) ;
712+ } else {
713+ const next = OPENAI_FIELDS [ idx + 1 ] ! ;
714+ setOpenaiInputValue ( openaiDisplayValues [ next ] ?? '' ) ;
715+ setOpenaiInputCursorOffset ( ( openaiDisplayValues [ next ] ?? '' ) . length ) ;
716+ }
717+ } , [ activeField , openaiInputValue , buildOpenAIState , doOpenAISave , openaiDisplayValues , setOAuthStatus ] ) ;
718+
719+ useKeybinding ( 'tabs:next' , ( ) => {
720+ const idx = OPENAI_FIELDS . indexOf ( activeField ) ;
721+ if ( idx < OPENAI_FIELDS . length - 1 ) {
722+ setOAuthStatus ( buildOpenAIState ( activeField , openaiInputValue , OPENAI_FIELDS [ idx + 1 ] ) ) ;
723+ setOpenaiInputValue ( openaiDisplayValues [ OPENAI_FIELDS [ idx + 1 ] ! ] ?? '' ) ;
724+ setOpenaiInputCursorOffset ( ( openaiDisplayValues [ OPENAI_FIELDS [ idx + 1 ] ! ] ?? '' ) . length ) ;
725+ }
726+ } , { context : 'Tabs' } ) ;
727+ useKeybinding ( 'tabs:previous' , ( ) => {
728+ const idx = OPENAI_FIELDS . indexOf ( activeField ) ;
729+ if ( idx > 0 ) {
730+ setOAuthStatus ( buildOpenAIState ( activeField , openaiInputValue , OPENAI_FIELDS [ idx - 1 ] ) ) ;
731+ setOpenaiInputValue ( openaiDisplayValues [ OPENAI_FIELDS [ idx - 1 ] ! ] ?? '' ) ;
732+ setOpenaiInputCursorOffset ( ( openaiDisplayValues [ OPENAI_FIELDS [ idx - 1 ] ! ] ?? '' ) . length ) ;
733+ }
734+ } , { context : 'Tabs' } ) ;
735+ useKeybinding ( 'confirm:no' , ( ) => {
736+ setOAuthStatus ( { state : 'idle' } ) ;
737+ } , { context : 'Confirmation' } ) ;
738+
739+ const openaiColumns = useTerminalSize ( ) . columns - 20 ;
740+
741+ const renderOpenAIRow = ( field : OpenAIField , label : string , opts ?: { mask ?: boolean } ) => {
742+ const active = activeField === field ;
743+ const val = openaiDisplayValues [ field ] ;
744+ return < Box >
745+ < Text backgroundColor = { active ? 'suggestion' : undefined } color = { active ? 'inverseText' : undefined } > { ` ${ label } ` } </ Text >
746+ < Text > </ Text >
747+ { active
748+ ? < TextInput value = { openaiInputValue } onChange = { setOpenaiInputValue } onSubmit = { handleOpenAIEnter } cursorOffset = { openaiInputCursorOffset } onChangeCursorOffset = { setOpenaiInputCursorOffset } columns = { openaiColumns } mask = { opts ?. mask ? "*" : undefined } focus = { true } />
749+ : ( val
750+ ? < Text color = "success" > { opts ?. mask ? val . slice ( 0 , 8 ) + '·' . repeat ( Math . max ( 0 , val . length - 8 ) ) : val } </ Text >
751+ : null ) }
752+ </ Box > ;
753+ } ;
754+
755+ return < Box flexDirection = "column" gap = { 1 } >
756+ < Text bold = { true } > OpenAI Compatible API Setup</ Text >
757+ < Text dimColor > Configure an OpenAI Chat Completions compatible endpoint (e.g. Ollama, DeepSeek, vLLM).</ Text >
758+ < Box flexDirection = "column" gap = { 1 } >
759+ { renderOpenAIRow ( 'base_url' , 'Base URL ' ) }
760+ { renderOpenAIRow ( 'api_key' , 'API Key ' , { mask : true } ) }
761+ { renderOpenAIRow ( 'haiku_model' , 'Haiku ' ) }
762+ { renderOpenAIRow ( 'sonnet_model' , 'Sonnet ' ) }
763+ { renderOpenAIRow ( 'opus_model' , 'Opus ' ) }
764+ </ Box >
765+ < Text dimColor > Tab to switch · Enter on last field to save · Esc to go back</ Text >
766+ </ Box > ;
767+ }
642768 case "waiting_for_login" :
643769 {
644770 let t1 ;
0 commit comments