1+ import { join } from 'node:path' ;
2+
13import {
4+ applyOpenAICodexConfig ,
25 applyOpenPlatformConfig ,
6+ FileTokenStorage ,
7+ fetchOpenAICodexModels ,
38 fetchOpenPlatformModels ,
49 filterModelsByPrefix ,
510 getOpenPlatformById ,
11+ loginOpenAICodexDeviceCode ,
12+ OPENAI_CODEX_MODELS ,
13+ OPENAI_CODEX_OAUTH_KEY ,
14+ OPENAI_CODEX_PROVIDER_NAME ,
615 OpenPlatformApiError ,
16+ resolveKimiTokenStorageName ,
717 type ManagedKimiCodeModelInfo ,
818 type ManagedKimiConfigShape ,
19+ type OpenAICodexModelInfo ,
920 type OpenPlatformDefinition ,
1021} from '@moonshot-ai/kimi-code-oauth' ;
11- import { log } from '@moonshot-ai/kimi-code-sdk' ;
22+ import { log , type ModelAlias } from '@moonshot-ai/kimi-code-sdk' ;
1223
1324import type { ChoiceOption } from '../components/dialogs/choice-picker' ;
1425import { DEFAULT_OAUTH_PROVIDER_NAME , PRODUCT_NAME } from '../constant/kimi-tui' ;
@@ -19,6 +30,7 @@ import {
1930 promptLogoutProviderSelection ,
2031 promptModelSelectionForOpenPlatform ,
2132 promptPlatformSelection ,
33+ runModelSelector ,
2234} from './prompts' ;
2335import type { SlashCommandHost } from './dispatch' ;
2436
@@ -34,12 +46,131 @@ export async function handleLoginCommand(host: SlashCommandHost): Promise<void>
3446 await handleKimiCodeOAuthLogin ( host ) ;
3547 return ;
3648 }
49+ if ( platformId === OPENAI_CODEX_PROVIDER_NAME ) {
50+ await handleOpenAICodexOAuthLogin ( host ) ;
51+ return ;
52+ }
3753
3854 const platform = getOpenPlatformById ( platformId ) ;
3955 if ( platform === undefined ) return ;
4056 await handleOpenPlatformLogin ( host , platform ) ;
4157}
4258
59+ async function handleOpenAICodexOAuthLogin ( host : SlashCommandHost ) : Promise < void > {
60+ let spinner : LoginProgressSpinnerHandle | undefined ;
61+ const tokenStorage = new FileTokenStorage ( join ( host . harness . homeDir , 'credentials' ) ) ;
62+ const tokenStorageName = resolveKimiTokenStorageName ( {
63+ providerName : OPENAI_CODEX_PROVIDER_NAME ,
64+ oauthKey : OPENAI_CODEX_OAUTH_KEY ,
65+ } ) ;
66+ const controller = new AbortController ( ) ;
67+ const cancelLogin = ( ) : void => {
68+ controller . abort ( ) ;
69+ } ;
70+ host . cancelInFlight = cancelLogin ;
71+ let openAICodexModels : readonly OpenAICodexModelInfo [ ] = OPENAI_CODEX_MODELS ;
72+ try {
73+ const token = await loginOpenAICodexDeviceCode ( {
74+ signal : controller . signal ,
75+ onDeviceCode : ( data ) => {
76+ spinner = host . showLoginAuthorizationPrompt (
77+ {
78+ userCode : data . userCode ,
79+ deviceCode : '' ,
80+ verificationUri : data . verificationUri ,
81+ verificationUriComplete : data . verificationUri ,
82+ expiresIn : data . expiresIn ,
83+ interval : data . interval ,
84+ } ,
85+ { title : 'Sign in to ChatGPT' } ,
86+ ) ;
87+ } ,
88+ } ) ;
89+ await tokenStorage . save ( tokenStorageName , token ) ;
90+ try {
91+ openAICodexModels = await fetchOpenAICodexModels ( token . accessToken , {
92+ signal : controller . signal ,
93+ } ) ;
94+ } catch ( error ) {
95+ log . warn ( 'failed to fetch OpenAI Codex models; using built-in fallback' , {
96+ providerName : OPENAI_CODEX_PROVIDER_NAME ,
97+ sessionId : host . session ?. id ,
98+ error,
99+ } ) ;
100+ }
101+ spinner ?. stop ( { ok : true , label : 'Logged in.' } ) ;
102+ spinner = undefined ;
103+ } catch ( error ) {
104+ const cancelled = controller . signal . aborted ;
105+ spinner ?. stop ( {
106+ ok : false ,
107+ label : cancelled ? 'Login cancelled.' : 'Login failed.' ,
108+ } ) ;
109+ spinner = undefined ;
110+ if ( ! cancelled ) {
111+ log . warn ( 'login failed' , {
112+ providerName : OPENAI_CODEX_PROVIDER_NAME ,
113+ sessionId : host . session ?. id ,
114+ error,
115+ } ) ;
116+ host . showError ( `Login failed: ${ formatErrorMessage ( error ) } ` ) ;
117+ }
118+ return ;
119+ } finally {
120+ if ( host . cancelInFlight === cancelLogin ) {
121+ host . cancelInFlight = undefined ;
122+ }
123+ }
124+
125+ const modelDict : Record < string , ModelAlias > = { } ;
126+ for ( const model of openAICodexModels ) {
127+ const supportEfforts = [ ...( model . supportEfforts ?? [ 'low' , 'medium' , 'high' , 'xhigh' ] ) ] ;
128+ modelDict [ `${ OPENAI_CODEX_PROVIDER_NAME } /${ model . id } ` ] = {
129+ provider : OPENAI_CODEX_PROVIDER_NAME ,
130+ model : model . id ,
131+ maxContextSize : model . contextLength ,
132+ capabilities : model . supportsImageIn
133+ ? [ 'tool_use' , 'thinking' , 'image_in' ]
134+ : [ 'tool_use' , 'thinking' ] ,
135+ displayName : model . displayName ,
136+ supportEfforts,
137+ defaultEffort : model . defaultEffort ?? ( supportEfforts . includes ( 'medium' ) ? 'medium' : supportEfforts [ 0 ] ) ,
138+ } ;
139+ }
140+ const selection = await runModelSelector ( host , modelDict ) ;
141+ if ( selection === undefined ) {
142+ await tokenStorage . remove ( tokenStorageName ) ;
143+ return ;
144+ }
145+
146+ const selectedModelId = selection . alias . slice ( `${ OPENAI_CODEX_PROVIDER_NAME } /` . length ) ;
147+ const existingConfig = await host . harness . getConfig ( ) ;
148+ if ( existingConfig . providers [ OPENAI_CODEX_PROVIDER_NAME ] !== undefined ) {
149+ await host . harness . removeProvider ( OPENAI_CODEX_PROVIDER_NAME ) ;
150+ }
151+
152+ const config = await host . harness . getConfig ( ) ;
153+ applyOpenAICodexConfig ( config as ManagedKimiConfigShape , {
154+ selectedModelId,
155+ thinking : selection . thinking !== 'off' ,
156+ effort :
157+ selection . thinking !== 'off' && selection . thinking !== 'on'
158+ ? selection . thinking
159+ : undefined ,
160+ models : openAICodexModels ,
161+ } ) ;
162+ await host . harness . setConfig ( {
163+ providers : config . providers ,
164+ models : config . models ,
165+ defaultModel : config . defaultModel ,
166+ thinking : config . thinking ,
167+ } ) ;
168+
169+ await host . authFlow . refreshConfigAfterLogin ( ) ;
170+ host . track ( 'login' , { provider : OPENAI_CODEX_PROVIDER_NAME , method : 'oauth' } ) ;
171+ host . showStatus ( `Setup complete: OpenAI ChatGPT Plus/Pro · ${ selectedModelId } ` ) ;
172+ }
173+
43174async function handleKimiCodeOAuthLogin ( host : SlashCommandHost ) : Promise < void > {
44175 const status = await host . harness . auth . status ( DEFAULT_OAUTH_PROVIDER_NAME ) ;
45176 const alreadyLoggedIn = status . providers . some (
@@ -222,6 +353,14 @@ export async function handleLogoutCommand(host: SlashCommandHost): Promise<void>
222353 if ( target === DEFAULT_OAUTH_PROVIDER_NAME ) {
223354 await host . harness . auth . logout ( DEFAULT_OAUTH_PROVIDER_NAME ) ;
224355 } else {
356+ if ( target === OPENAI_CODEX_PROVIDER_NAME ) {
357+ await new FileTokenStorage ( join ( host . harness . homeDir , 'credentials' ) ) . remove (
358+ resolveKimiTokenStorageName ( {
359+ providerName : OPENAI_CODEX_PROVIDER_NAME ,
360+ oauthKey : OPENAI_CODEX_OAUTH_KEY ,
361+ } ) ,
362+ ) ;
363+ }
225364 await host . harness . removeProvider ( target ) ;
226365 }
227366
0 commit comments