@@ -14,6 +14,7 @@ import { isSupportedByokProviderId } from "../lib/byok-providers.js";
1414import { logger } from "../lib/logger.js" ;
1515import type { OpenClawProcessManager } from "../runtime/openclaw-process.js" ;
1616import type { NexuConfigStore } from "../store/nexu-config-store.js" ;
17+ import type { OpenClawAuthService } from "./openclaw-auth-service.js" ;
1718import type { OpenClawSyncService } from "./openclaw-sync-service.js" ;
1819
1920export interface ModelAutoSelectResult {
@@ -255,7 +256,12 @@ function getOpenClawCommandSpec(env: ControllerEnv): {
255256 } ;
256257}
257258
259+ // Providers that support OAuth login (no API key needed).
260+ const OAUTH_PROVIDER_IDS = new Set ( [ "openai" ] ) ;
261+
258262export class ModelProviderService {
263+ private openclawAuthService : OpenClawAuthService | null = null ;
264+
259265 private miniMaxOauthAbortController : AbortController | null = null ;
260266
261267 private miniMaxOauthBrowserUrl : string | null = null ;
@@ -287,35 +293,89 @@ export class ModelProviderService {
287293 private readonly openclawProcess : OpenClawProcessManager ,
288294 ) { }
289295
296+ /**
297+ * Inject the auth service after construction to avoid circular deps.
298+ */
299+ setAuthService ( authService : OpenClawAuthService ) : void {
300+ this . openclawAuthService = authService ;
301+ }
302+
290303 async listModels ( ) {
291304 await this . refreshMiniMaxOauthModelsIfNeeded ( ) ;
292305
293306 const config = await this . configStore . getConfig ( ) ;
294307 const desktopCloud = await this . configStore . getDesktopCloudStatus ( ) ;
295- const cloudModels : Model [ ] = ( desktopCloud . models ?? [ ] ) . map ( ( model ) => ( {
296- id : model . id ,
297- name : model . name || model . id ,
298- provider : "nexu" ,
299- description : "Cloud model via Nexu Link" ,
300- } ) ) ;
301-
302308 const providers = config . providers . filter (
303309 ( provider ) =>
304310 provider . enabled && isSupportedByokProviderId ( provider . providerId ) ,
305311 ) ;
306- const byokModels : Model [ ] = providers . flatMap ( ( provider ) =>
307- provider . models . map ( ( modelId ) => ( {
308- id : `${ provider . providerId } /${ modelId } ` ,
309- name : modelId ,
310- provider : provider . providerId ,
311- } ) ) ,
312+ const { cloudModels, byokModels } = await this . getAvailableModels (
313+ providers ,
314+ desktopCloud ,
312315 ) ;
313316
314317 return {
315318 models : [ ...cloudModels , ...byokModels ] ,
316319 } ;
317320 }
318321
322+ private async getAvailableModels (
323+ providers : ReadonlyArray < {
324+ providerId : string ;
325+ apiKey : string | null ;
326+ models : string [ ] ;
327+ } > ,
328+ desktopCloud : {
329+ models ?: Array < { id : string ; name ?: string | null } > | null ;
330+ } ,
331+ ) : Promise < { cloudModels : Model [ ] ; byokModels : Model [ ] } > {
332+ const cloudModels : Model [ ] = ( desktopCloud . models ?? [ ] ) . map ( ( model ) => ( {
333+ id : model . id ,
334+ name : model . name || model . id ,
335+ provider : "nexu" ,
336+ description : "Cloud model via Nexu Link" ,
337+ } ) ) ;
338+
339+ // Exclude OAuth-only providers whose token has expired
340+ const expiredOAuthProviderIds =
341+ await this . getExpiredOAuthProviderIds ( providers ) ;
342+
343+ const byokModels : Model [ ] = providers
344+ . filter ( ( provider ) => ! expiredOAuthProviderIds . has ( provider . providerId ) )
345+ . flatMap ( ( provider ) =>
346+ provider . models . map ( ( modelId ) => ( {
347+ id : `${ provider . providerId } /${ modelId } ` ,
348+ name : modelId ,
349+ provider : provider . providerId ,
350+ } ) ) ,
351+ ) ;
352+
353+ return { cloudModels, byokModels } ;
354+ }
355+
356+ /**
357+ * Returns provider IDs that use OAuth (no API key) and whose token is expired.
358+ */
359+ private async getExpiredOAuthProviderIds (
360+ providers : ReadonlyArray < { providerId : string ; apiKey : string | null } > ,
361+ ) : Promise < Set < string > > {
362+ if ( ! this . openclawAuthService ) return new Set ( ) ;
363+
364+ const expired = new Set < string > ( ) ;
365+ for ( const provider of providers ) {
366+ if ( provider . apiKey || ! OAUTH_PROVIDER_IDS . has ( provider . providerId ) ) {
367+ continue ;
368+ }
369+ const status = await this . openclawAuthService . getProviderOAuthStatus (
370+ provider . providerId ,
371+ ) ;
372+ if ( ! status . connected ) {
373+ expired . add ( provider . providerId ) ;
374+ }
375+ }
376+ return expired ;
377+ }
378+
319379 async listProviders ( ) {
320380 await this . refreshMiniMaxOauthModelsIfNeeded ( ) ;
321381
@@ -576,18 +636,9 @@ export class ModelProviderService {
576636 return "unknown" ;
577637 }
578638
579- const cloudModels : Model [ ] = ( desktopCloud . models ?? [ ] ) . map ( ( model ) => ( {
580- id : model . id ,
581- name : model . name || model . id ,
582- provider : "nexu" ,
583- description : "Cloud model via Nexu Link" ,
584- } ) ) ;
585- const byokModels : Model [ ] = providers . flatMap ( ( provider ) =>
586- provider . models . map ( ( modelId ) => ( {
587- id : `${ provider . providerId } /${ modelId } ` ,
588- name : modelId ,
589- provider : provider . providerId ,
590- } ) ) ,
639+ const { cloudModels, byokModels } = await this . getAvailableModels (
640+ providers ,
641+ desktopCloud ,
591642 ) ;
592643 const knownModels = [ ...cloudModels , ...byokModels ] ;
593644
0 commit comments