@@ -9,9 +9,14 @@ import { setClipboard, useTerminalNotification, Box, Link, Text, KeyboardShortcu
99import { useKeybinding } from '../keybindings/useKeybinding.js' ;
1010import { getSSLErrorHint } from '@ant/model-provider' ;
1111import { sendNotification } from '../services/notifier.js' ;
12+ import {
13+ completeChatGPTDeviceLogin ,
14+ requestChatGPTDeviceCode ,
15+ type ChatGPTDeviceCode ,
16+ } from '../services/api/openai/chatgptAuth.js' ;
1217import { OAuthService } from '../services/oauth/index.js' ;
1318import { getOauthAccountInfo , validateForceLoginOrg } from '../utils/auth.js' ;
14-
19+ import { openBrowser } from '../utils/browser.js' ;
1520import { logError } from '../utils/log.js' ;
1621import { getSettings_DEPRECATED , updateSettingsForSource } from '../utils/settings/settings.js' ;
1722import { Select } from './CustomSelect/select.js' ;
@@ -46,6 +51,11 @@ type OAuthStatus =
4651 opusModel : string ;
4752 activeField : 'base_url' | 'api_key' | 'haiku_model' | 'sonnet_model' | 'opus_model' ;
4853 } // OpenAI Chat Completions API platform
54+ | {
55+ state : 'chatgpt_subscription' ;
56+ phase : 'requesting' | 'waiting' ;
57+ deviceCode ?: ChatGPTDeviceCode ;
58+ } // ChatGPT account subscription via Codex OAuth device flow
4959 | {
5060 state : 'gemini_api' ;
5161 baseUrl : string ;
@@ -445,6 +455,15 @@ function OAuthStatusMessage({
445455 ) ,
446456 value : 'openai_chat_api' ,
447457 } ,
458+ {
459+ label : (
460+ < Text >
461+ ChatGPT account with subscription · < Text dimColor > Plus, Pro, Business, Edu, or Enterprise</ Text >
462+ { '\n' }
463+ </ Text >
464+ ) ,
465+ value : 'chatgpt_subscription' ,
466+ } ,
448467 {
449468 label : (
450469 < Text >
@@ -515,6 +534,12 @@ function OAuthStatusMessage({
515534 opusModel : process . env . OPENAI_DEFAULT_OPUS_MODEL ?? '' ,
516535 activeField : 'base_url' ,
517536 } ) ;
537+ } else if ( value === 'chatgpt_subscription' ) {
538+ logEvent ( 'tengu_chatgpt_subscription_selected' , { } ) ;
539+ setOAuthStatus ( {
540+ state : 'chatgpt_subscription' ,
541+ phase : 'requesting' ,
542+ } ) ;
518543 } else if ( value === 'gemini_api' ) {
519544 logEvent ( 'tengu_gemini_api_selected' , { } ) ;
520545 setOAuthStatus ( {
@@ -807,7 +832,9 @@ function OAuthStatusMessage({
807832
808833 const doOpenAISave = useCallback ( ( ) => {
809834 const finalVals = { ...openaiDisplayValues , [ activeField ] : openaiInputValue } ;
810- const env : Record < string , string > = { } ;
835+ const env : Record < string , string | undefined > = {
836+ OPENAI_AUTH_MODE : undefined ,
837+ } ;
811838
812839 // Validate base_url if provided
813840 if ( finalVals . base_url ) {
@@ -836,10 +863,11 @@ function OAuthStatusMessage({
836863 if ( finalVals . haiku_model ) env . OPENAI_DEFAULT_HAIKU_MODEL = finalVals . haiku_model ;
837864 if ( finalVals . sonnet_model ) env . OPENAI_DEFAULT_SONNET_MODEL = finalVals . sonnet_model ;
838865 if ( finalVals . opus_model ) env . OPENAI_DEFAULT_OPUS_MODEL = finalVals . opus_model ;
839- const { error } = updateSettingsForSource ( 'userSettings' , {
840- modelType : 'openai' as any ,
841- env,
842- } as any ) ;
866+ const settingsUpdate : Parameters < typeof updateSettingsForSource > [ 1 ] = {
867+ modelType : 'openai' ,
868+ env : env as unknown as Record < string , string > ,
869+ } ;
870+ const { error } = updateSettingsForSource ( 'userSettings' , settingsUpdate ) ;
843871 if ( error ) {
844872 setOAuthStatus ( {
845873 state : 'error' ,
@@ -855,7 +883,13 @@ function OAuthStatusMessage({
855883 } ,
856884 } ) ;
857885 } else {
858- for ( const [ k , v ] of Object . entries ( env ) ) process . env [ k ] = v ;
886+ for ( const [ k , v ] of Object . entries ( env ) ) {
887+ if ( v === undefined ) {
888+ delete process . env [ k ] ;
889+ } else {
890+ process . env [ k ] = v ;
891+ }
892+ }
859893 setOAuthStatus ( { state : 'success' } ) ;
860894 void onDone ( ) ;
861895 }
@@ -953,6 +987,93 @@ function OAuthStatusMessage({
953987 ) ;
954988 }
955989
990+ case 'chatgpt_subscription' : {
991+ const status = oauthStatus as {
992+ state : 'chatgpt_subscription' ;
993+ phase : 'requesting' | 'waiting' ;
994+ deviceCode ?: ChatGPTDeviceCode ;
995+ } ;
996+ const startedRef = useRef ( false ) ;
997+
998+ useEffect ( ( ) => {
999+ if ( startedRef . current ) return ;
1000+ startedRef . current = true ;
1001+ let cancelled = false ;
1002+ const controller = new AbortController ( ) ;
1003+ async function runLogin ( ) {
1004+ try {
1005+ const deviceCode = await requestChatGPTDeviceCode ( ) ;
1006+ if ( cancelled ) return ;
1007+ setOAuthStatus ( {
1008+ state : 'chatgpt_subscription' ,
1009+ phase : 'waiting' ,
1010+ deviceCode,
1011+ } ) ;
1012+ void openBrowser ( deviceCode . verificationUrl ) ;
1013+ await completeChatGPTDeviceLogin ( deviceCode , controller . signal ) ;
1014+ if ( cancelled ) return ;
1015+ const env : Record < string , string > = {
1016+ OPENAI_AUTH_MODE : 'chatgpt' ,
1017+ } ;
1018+ const settingsUpdate : Parameters < typeof updateSettingsForSource > [ 1 ] = {
1019+ modelType : 'openai' ,
1020+ env,
1021+ } ;
1022+ const { error } = updateSettingsForSource ( 'userSettings' , settingsUpdate ) ;
1023+ if ( error ) {
1024+ throw new Error ( 'Failed to save settings. Please try again.' ) ;
1025+ }
1026+ for ( const [ k , v ] of Object . entries ( env ) ) process . env [ k ] = v ;
1027+ setOAuthStatus ( { state : 'success' } ) ;
1028+ void onDone ( ) ;
1029+ } catch ( err ) {
1030+ if ( cancelled ) return ;
1031+ setOAuthStatus ( {
1032+ state : 'error' ,
1033+ message : ( err as Error ) . message ,
1034+ toRetry : {
1035+ state : 'chatgpt_subscription' ,
1036+ phase : 'requesting' ,
1037+ } ,
1038+ } ) ;
1039+ }
1040+ }
1041+ void runLogin ( ) ;
1042+ return ( ) => {
1043+ cancelled = true ;
1044+ controller . abort ( ) ;
1045+ } ;
1046+ } , [ setOAuthStatus , onDone ] ) ;
1047+
1048+ return (
1049+ < Box flexDirection = "column" gap = { 1 } >
1050+ < Text bold > ChatGPT Account Setup</ Text >
1051+ { status . phase === 'requesting' && (
1052+ < Box >
1053+ < Spinner />
1054+ < Text > Requesting sign-in code…</ Text >
1055+ </ Box >
1056+ ) }
1057+ { status . phase === 'waiting' && status . deviceCode && (
1058+ < Box flexDirection = "column" gap = { 1 } >
1059+ < Text > Open this link and sign in with your ChatGPT account:</ Text >
1060+ < Link url = { status . deviceCode . verificationUrl } >
1061+ < Text dimColor > { status . deviceCode . verificationUrl } </ Text >
1062+ </ Link >
1063+ < Text >
1064+ Enter code: < Text bold > { status . deviceCode . userCode } </ Text >
1065+ </ Text >
1066+ < Box >
1067+ < Spinner />
1068+ < Text > Waiting for ChatGPT authorization…</ Text >
1069+ </ Box >
1070+ </ Box >
1071+ ) }
1072+ < Text dimColor > Esc to go back. Device codes expire after 15 minutes.</ Text >
1073+ </ Box >
1074+ ) ;
1075+ }
1076+
9561077 case 'gemini_api' : {
9571078 type GeminiField = 'base_url' | 'api_key' | 'haiku_model' | 'sonnet_model' | 'opus_model' ;
9581079 const GEMINI_FIELDS : GeminiField [ ] = [ 'base_url' , 'api_key' , 'haiku_model' , 'sonnet_model' , 'opus_model' ] ;
0 commit comments