@@ -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,16 @@ function OAuthStatusMessage({
445455 ) ,
446456 value : 'openai_chat_api' ,
447457 } ,
458+ {
459+ label : (
460+ < Text >
461+ ChatGPT account with subscription ·{ ' ' }
462+ < Text dimColor > Plus, Pro, Business, Edu, or Enterprise</ Text >
463+ { '\n' }
464+ </ Text >
465+ ) ,
466+ value : 'chatgpt_subscription' ,
467+ } ,
448468 {
449469 label : (
450470 < Text >
@@ -515,6 +535,12 @@ function OAuthStatusMessage({
515535 opusModel : process . env . OPENAI_DEFAULT_OPUS_MODEL ?? '' ,
516536 activeField : 'base_url' ,
517537 } ) ;
538+ } else if ( value === 'chatgpt_subscription' ) {
539+ logEvent ( 'tengu_chatgpt_subscription_selected' , { } ) ;
540+ setOAuthStatus ( {
541+ state : 'chatgpt_subscription' ,
542+ phase : 'requesting' ,
543+ } ) ;
518544 } else if ( value === 'gemini_api' ) {
519545 logEvent ( 'tengu_gemini_api_selected' , { } ) ;
520546 setOAuthStatus ( {
@@ -807,7 +833,9 @@ function OAuthStatusMessage({
807833
808834 const doOpenAISave = useCallback ( ( ) => {
809835 const finalVals = { ...openaiDisplayValues , [ activeField ] : openaiInputValue } ;
810- const env : Record < string , string > = { } ;
836+ const env : Record < string , string | undefined > = {
837+ OPENAI_AUTH_MODE : undefined ,
838+ } ;
811839
812840 // Validate base_url if provided
813841 if ( finalVals . base_url ) {
@@ -836,10 +864,11 @@ function OAuthStatusMessage({
836864 if ( finalVals . haiku_model ) env . OPENAI_DEFAULT_HAIKU_MODEL = finalVals . haiku_model ;
837865 if ( finalVals . sonnet_model ) env . OPENAI_DEFAULT_SONNET_MODEL = finalVals . sonnet_model ;
838866 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 ) ;
867+ const settingsUpdate : Parameters < typeof updateSettingsForSource > [ 1 ] = {
868+ modelType : 'openai' ,
869+ env : env as unknown as Record < string , string > ,
870+ } ;
871+ const { error } = updateSettingsForSource ( 'userSettings' , settingsUpdate ) ;
843872 if ( error ) {
844873 setOAuthStatus ( {
845874 state : 'error' ,
@@ -855,7 +884,13 @@ function OAuthStatusMessage({
855884 } ,
856885 } ) ;
857886 } else {
858- for ( const [ k , v ] of Object . entries ( env ) ) process . env [ k ] = v ;
887+ for ( const [ k , v ] of Object . entries ( env ) ) {
888+ if ( v === undefined ) {
889+ delete process . env [ k ] ;
890+ } else {
891+ process . env [ k ] = v ;
892+ }
893+ }
859894 setOAuthStatus ( { state : 'success' } ) ;
860895 void onDone ( ) ;
861896 }
@@ -953,6 +988,93 @@ function OAuthStatusMessage({
953988 ) ;
954989 }
955990
991+ case 'chatgpt_subscription' : {
992+ const status = oauthStatus as {
993+ state : 'chatgpt_subscription' ;
994+ phase : 'requesting' | 'waiting' ;
995+ deviceCode ?: ChatGPTDeviceCode ;
996+ } ;
997+ const startedRef = useRef ( false ) ;
998+
999+ useEffect ( ( ) => {
1000+ if ( startedRef . current ) return ;
1001+ startedRef . current = true ;
1002+ let cancelled = false ;
1003+ const controller = new AbortController ( ) ;
1004+ async function runLogin ( ) {
1005+ try {
1006+ const deviceCode = await requestChatGPTDeviceCode ( ) ;
1007+ if ( cancelled ) return ;
1008+ setOAuthStatus ( {
1009+ state : 'chatgpt_subscription' ,
1010+ phase : 'waiting' ,
1011+ deviceCode,
1012+ } ) ;
1013+ void openBrowser ( deviceCode . verificationUrl ) ;
1014+ await completeChatGPTDeviceLogin ( deviceCode , controller . signal ) ;
1015+ if ( cancelled ) return ;
1016+ const env : Record < string , string > = {
1017+ OPENAI_AUTH_MODE : 'chatgpt' ,
1018+ } ;
1019+ const settingsUpdate : Parameters < typeof updateSettingsForSource > [ 1 ] = {
1020+ modelType : 'openai' ,
1021+ env,
1022+ } ;
1023+ const { error } = updateSettingsForSource ( 'userSettings' , settingsUpdate ) ;
1024+ if ( error ) {
1025+ throw new Error ( 'Failed to save settings. Please try again.' ) ;
1026+ }
1027+ for ( const [ k , v ] of Object . entries ( env ) ) process . env [ k ] = v ;
1028+ setOAuthStatus ( { state : 'success' } ) ;
1029+ void onDone ( ) ;
1030+ } catch ( err ) {
1031+ if ( cancelled ) return ;
1032+ setOAuthStatus ( {
1033+ state : 'error' ,
1034+ message : ( err as Error ) . message ,
1035+ toRetry : {
1036+ state : 'chatgpt_subscription' ,
1037+ phase : 'requesting' ,
1038+ } ,
1039+ } ) ;
1040+ }
1041+ }
1042+ void runLogin ( ) ;
1043+ return ( ) => {
1044+ cancelled = true ;
1045+ controller . abort ( ) ;
1046+ } ;
1047+ } , [ setOAuthStatus , onDone ] ) ;
1048+
1049+ return (
1050+ < Box flexDirection = "column" gap = { 1 } >
1051+ < Text bold > ChatGPT Account Setup</ Text >
1052+ { status . phase === 'requesting' && (
1053+ < Box >
1054+ < Spinner />
1055+ < Text > Requesting sign-in code…</ Text >
1056+ </ Box >
1057+ ) }
1058+ { status . phase === 'waiting' && status . deviceCode && (
1059+ < Box flexDirection = "column" gap = { 1 } >
1060+ < Text > Open this link and sign in with your ChatGPT account:</ Text >
1061+ < Link url = { status . deviceCode . verificationUrl } >
1062+ < Text dimColor > { status . deviceCode . verificationUrl } </ Text >
1063+ </ Link >
1064+ < Text >
1065+ Enter code: < Text bold > { status . deviceCode . userCode } </ Text >
1066+ </ Text >
1067+ < Box >
1068+ < Spinner />
1069+ < Text > Waiting for ChatGPT authorization…</ Text >
1070+ </ Box >
1071+ </ Box >
1072+ ) }
1073+ < Text dimColor > Esc to go back. Device codes expire after 15 minutes.</ Text >
1074+ </ Box >
1075+ ) ;
1076+ }
1077+
9561078 case 'gemini_api' : {
9571079 type GeminiField = 'base_url' | 'api_key' | 'haiku_model' | 'sonnet_model' | 'opus_model' ;
9581080 const GEMINI_FIELDS : GeminiField [ ] = [ 'base_url' , 'api_key' , 'haiku_model' , 'sonnet_model' , 'opus_model' ] ;
0 commit comments