11import {
2+ applyKimiOAuthConfig ,
3+ applyMiniMaxOAuthConfig ,
24 applyOpenAICodexOAuthConfig ,
35 applyOpenPlatformConfig ,
6+ fetchKimiCodingModels ,
47 fetchOpenAICodexModels ,
58 fetchOpenPlatformModels ,
69 filterModelsByPrefix ,
710 getOpenPlatformById ,
11+ miniMaxCodingModels ,
12+ minimaxCodingProviderId ,
13+ minimaxRegionLabel ,
14+ KIMI_CODING_PROVIDER_ID ,
15+ KIMI_OAUTH_PLATFORM_ID ,
16+ MINIMAX_OAUTH_PLATFORM_ID_CN ,
17+ MINIMAX_OAUTH_PLATFORM_ID_GLOBAL ,
818 OPENAI_CODEX_OAUTH_PLATFORM_ID ,
919 OPENAI_CODEX_PROVIDER_ID ,
20+ OAuthAccessDeniedError ,
1021 OpenAICodexApiError ,
1122 OpenPlatformApiError ,
12- OAuthAccessDeniedError ,
23+ runKimiOAuthFlow ,
24+ runMiniMaxOAuthFlow ,
1325 runOpenAICodexOAuthFlow ,
14- type ProviderModelInfo ,
15- type PythinkerConfigShape ,
26+ type DeviceCodeInfo ,
27+ type MiniMaxRegion ,
1628 type OpenAICodexModelInfo ,
1729 type OpenPlatformDefinition ,
30+ type ProviderModelInfo ,
31+ type PythinkerConfigShape ,
1832} from '@pymodel/pythinker-code-oauth' ;
1933
2034import {
@@ -29,7 +43,7 @@ import type { PythinkerConfig } from '#/types';
2943
3044import { formatErrorMessage } from '../error-format' ;
3145import { catalogProviderIdFromPlatformValue } from './platform-values' ;
32- import type { LoginUi } from './types' ;
46+ import type { LoginProgressSpinnerHandle , LoginUi } from './types' ;
3347
3448export async function runLogin ( ui : LoginUi ) : Promise < boolean > {
3549 const selection = await ui . promptPlatformSelection ( ) ;
@@ -43,6 +57,15 @@ export async function runLogin(ui: LoginUi): Promise<boolean> {
4357 if ( platformId === OPENAI_CODEX_OAUTH_PLATFORM_ID ) {
4458 return handleOpenAICodexOAuthLogin ( ui ) ;
4559 }
60+ if ( platformId === KIMI_OAUTH_PLATFORM_ID ) {
61+ return handleKimiOAuthLogin ( ui ) ;
62+ }
63+ if ( platformId === MINIMAX_OAUTH_PLATFORM_ID_GLOBAL ) {
64+ return handleMiniMaxOAuthLogin ( ui , 'global' ) ;
65+ }
66+ if ( platformId === MINIMAX_OAUTH_PLATFORM_ID_CN ) {
67+ return handleMiniMaxOAuthLogin ( ui , 'cn' ) ;
68+ }
4669 const platform = getOpenPlatformById ( platformId ) ;
4770 return platform === undefined ? false : handleOpenPlatformLogin ( ui , platform ) ;
4871}
@@ -262,6 +285,158 @@ async function handleOpenAICodexOAuthLogin(ui: LoginUi): Promise<boolean> {
262285 }
263286}
264287
288+ async function handleKimiOAuthLogin ( ui : LoginUi ) : Promise < boolean > {
289+ const controller = new AbortController ( ) ;
290+ let committing = false ;
291+ const cancelLogin = ( ) : void => {
292+ if ( ! committing ) controller . abort ( ) ;
293+ } ;
294+ ui . cancelInFlight = cancelLogin ;
295+ try {
296+ let spinner : LoginProgressSpinnerHandle | undefined ;
297+ let tokens ;
298+ try {
299+ tokens = await runKimiOAuthFlow ( {
300+ signal : controller . signal ,
301+ onCodeReady : ( info : DeviceCodeInfo ) => {
302+ ui . openBrowser ( info . verificationUriComplete ?? info . verificationUri ) ;
303+ spinner = ui . showLoginProgressSpinner (
304+ `Waiting for authorization — open ${ info . verificationUri } and enter code ${ info . userCode } ` ,
305+ ) ;
306+ } ,
307+ } ) ;
308+ } catch ( error ) {
309+ spinner ?. stop ( { ok : false , label : 'Sign-in failed.' } ) ;
310+ if ( controller . signal . aborted ) return false ;
311+ if ( error instanceof OAuthAccessDeniedError ) {
312+ ui . showError ( `Kimi login cancelled: ${ formatErrorMessage ( error ) } ` ) ;
313+ return false ;
314+ }
315+ ui . showError ( `Kimi login failed: ${ formatErrorMessage ( error ) } ` ) ;
316+ return false ;
317+ }
318+ spinner ?. stop ( { ok : true , label : 'Authorized.' } ) ;
319+
320+ let models : ProviderModelInfo [ ] ;
321+ try {
322+ models = await fetchKimiCodingModels ( tokens . accessToken , tokens . deviceId , fetch , controller . signal ) ;
323+ } catch ( error ) {
324+ if ( controller . signal . aborted ) return false ;
325+ ui . showError ( `Failed to list Kimi For Coding models: ${ formatErrorMessage ( error ) } ` ) ;
326+ return false ;
327+ }
328+ if ( models . length === 0 ) {
329+ ui . showError ( 'No models available for Kimi For Coding.' ) ;
330+ return false ;
331+ }
332+
333+ const picked = await ui . promptModelSelectionForOpenPlatform ( models , {
334+ id : KIMI_CODING_PROVIDER_ID ,
335+ name : 'Kimi For Coding' ,
336+ } ) ;
337+ if ( picked === undefined ) return false ;
338+ const selectedModel = models . find ( ( model ) => model . id === picked . model . id ) ;
339+ if ( selectedModel === undefined ) return false ;
340+
341+ controller . signal . throwIfAborted ( ) ;
342+ const current = await ui . harness . getConfig ( { reload : true } ) ;
343+ controller . signal . throwIfAborted ( ) ;
344+ const next = cloneConfig ( current ) ;
345+ applyKimiOAuthConfig ( next as PythinkerConfigShape , {
346+ accessToken : tokens . accessToken ,
347+ refreshToken : tokens . refreshToken ,
348+ deviceId : tokens . deviceId ,
349+ models,
350+ selectedModel,
351+ thinking : picked . effort !== 'off' ,
352+ effort : picked . effort === 'off' || picked . effort === 'on' ? undefined : picked . effort ,
353+ } ) ;
354+ committing = true ;
355+ await ui . harness . replaceConfigSections ( {
356+ providers : next . providers ,
357+ models : next . models ,
358+ defaultModel : next . defaultModel ,
359+ thinking : next . thinking ,
360+ } ) ;
361+ await ui . refreshConfigAfterLogin ( ) ;
362+ ui . track ( 'login' , { provider : KIMI_CODING_PROVIDER_ID , method : 'oauth' } ) ;
363+ ui . showStatus ( `Setup complete: Kimi For Coding · ${ selectedModel . id } ` ) ;
364+ return true ;
365+ } finally {
366+ if ( ui . cancelInFlight === cancelLogin ) ui . cancelInFlight = undefined ;
367+ }
368+ }
369+
370+ async function handleMiniMaxOAuthLogin ( ui : LoginUi , region : MiniMaxRegion ) : Promise < boolean > {
371+ const controller = new AbortController ( ) ;
372+ let committing = false ;
373+ const cancelLogin = ( ) : void => {
374+ if ( ! committing ) controller . abort ( ) ;
375+ } ;
376+ ui . cancelInFlight = cancelLogin ;
377+ const regionLabel = minimaxRegionLabel ( region ) ;
378+ try {
379+ let spinner : LoginProgressSpinnerHandle | undefined ;
380+ let tokens ;
381+ try {
382+ tokens = await runMiniMaxOAuthFlow ( region , {
383+ signal : controller . signal ,
384+ onCodeReady : ( info : DeviceCodeInfo ) => {
385+ ui . openBrowser ( info . verificationUriComplete ?? info . verificationUri ) ;
386+ spinner = ui . showLoginProgressSpinner (
387+ `Waiting for authorization — open ${ info . verificationUri } and enter code ${ info . userCode } ` ,
388+ ) ;
389+ } ,
390+ } ) ;
391+ } catch ( error ) {
392+ spinner ?. stop ( { ok : false , label : 'Sign-in failed.' } ) ;
393+ if ( controller . signal . aborted ) return false ;
394+ if ( error instanceof OAuthAccessDeniedError ) {
395+ ui . showError ( `${ regionLabel } login cancelled: ${ formatErrorMessage ( error ) } ` ) ;
396+ return false ;
397+ }
398+ ui . showError ( `${ regionLabel } login failed: ${ formatErrorMessage ( error ) } ` ) ;
399+ return false ;
400+ }
401+ spinner ?. stop ( { ok : true , label : 'Authorized.' } ) ;
402+
403+ const providerId = minimaxCodingProviderId ( region ) ;
404+ const models = [ ...miniMaxCodingModels ( ) ] ;
405+ const picked = await ui . promptModelSelectionForOpenPlatform ( models , {
406+ id : providerId ,
407+ name : regionLabel ,
408+ } ) ;
409+ if ( picked === undefined ) return false ;
410+ const selectedModel = models . find ( ( model ) => model . id === picked . model . id ) ;
411+ if ( selectedModel === undefined ) return false ;
412+
413+ controller . signal . throwIfAborted ( ) ;
414+ const current = await ui . harness . getConfig ( { reload : true } ) ;
415+ controller . signal . throwIfAborted ( ) ;
416+ const next = cloneConfig ( current ) ;
417+ applyMiniMaxOAuthConfig ( next as PythinkerConfigShape , region , {
418+ accessToken : tokens . accessToken ,
419+ refreshToken : tokens . refreshToken ,
420+ selectedModel,
421+ thinking : picked . effort !== 'off' ,
422+ effort : picked . effort === 'off' || picked . effort === 'on' ? undefined : picked . effort ,
423+ } ) ;
424+ committing = true ;
425+ await ui . harness . replaceConfigSections ( {
426+ providers : next . providers ,
427+ models : next . models ,
428+ defaultModel : next . defaultModel ,
429+ thinking : next . thinking ,
430+ } ) ;
431+ await ui . refreshConfigAfterLogin ( ) ;
432+ ui . track ( 'login' , { provider : providerId , method : 'oauth' } ) ;
433+ ui . showStatus ( `Setup complete: ${ regionLabel } · ${ selectedModel . id } ` ) ;
434+ return true ;
435+ } finally {
436+ if ( ui . cancelInFlight === cancelLogin ) ui . cancelInFlight = undefined ;
437+ }
438+ }
439+
265440function cloneConfig ( config : PythinkerConfig ) : PythinkerConfig {
266441 return {
267442 ...config ,
0 commit comments