@@ -120,6 +120,65 @@ public static async Task<int> Main(string[] args)
120120 contextProvider . PermissionContext . Mode = resumedMode ;
121121 await contextProvider . LoadMemoryAsync ( ) ;
122122 var storedCredentials = nyxIdCredentialStore . Load ( ) ;
123+
124+ // First-run onboarding: unless the user is about to run a setup-fixing
125+ // subcommand (login/logout/llm/aevatar/storage), explicitly overrode
126+ // the provider/model, picked Ollama, or is resuming an existing
127+ // session, refuse to boot without a usable NyxID default. If we have
128+ // a TTY we walk the picker inline; otherwise we print guidance and
129+ // exit non-zero so CI doesn't hang on a prompt.
130+ var invocationBypassesOnboarding =
131+ IsSetupFixingSubcommand ( options . InitialPrompt ) ||
132+ ! string . IsNullOrWhiteSpace ( options . Provider ) ||
133+ ! string . IsNullOrWhiteSpace ( options . Model ) ||
134+ resumed != null ;
135+
136+ if ( ! invocationBypassesOnboarding )
137+ {
138+ if ( storedCredentials == null )
139+ {
140+ Console . Error . WriteLine (
141+ " 尚未登录 NyxID。请先运行 `aexon login`(或 `/login`),完成后再执行本次命令。" ) ;
142+ return 1 ;
143+ }
144+
145+ if ( string . IsNullOrWhiteSpace ( storedCredentials . DefaultProvider ) &&
146+ string . IsNullOrWhiteSpace ( storedCredentials . DefaultProxySlug ) )
147+ {
148+ var canPromptInteractively = ! options . PrintMode && ! Console . IsInputRedirected ;
149+ if ( ! canPromptInteractively )
150+ {
151+ Console . Error . WriteLine (
152+ " 尚未为 aexon 选择默认 LLM provider。请先运行 `aexon llm`(交互式挑选)\n " +
153+ " 或 `aexon llm use <provider> [model]`(非交互),再重跑本次命令。" ) ;
154+ return 1 ;
155+ }
156+
157+ using var onboardingStatusClient = new NyxIdLlmStatusClient ( nyxIdTokenProvider ) ;
158+ using var onboardingKeysClient = new NyxIdKeysClient ( nyxIdTokenProvider ) ;
159+ Console . WriteLine ( " 尚未选择默认 LLM provider,进入首次配置流程:" ) ;
160+ var updated = await NyxIdProviderPicker . RunAsync (
161+ nyxIdCredentialStore ,
162+ onboardingStatusClient ,
163+ onboardingKeysClient ,
164+ storedCredentials ,
165+ Console . WriteLine ,
166+ new SpectreProviderPickerUi ( ) ,
167+ CancellationToken . None ) ;
168+
169+ if ( updated == null ||
170+ ( string . IsNullOrWhiteSpace ( updated . DefaultProvider ) &&
171+ string . IsNullOrWhiteSpace ( updated . DefaultProxySlug ) ) )
172+ {
173+ Console . Error . WriteLine (
174+ " 未能完成 provider 选择。之后可以随时运行 `aexon llm` 重试。" ) ;
175+ return 1 ;
176+ }
177+
178+ storedCredentials = updated ;
179+ }
180+ }
181+
123182 var defaultProviderFromStore = string . IsNullOrWhiteSpace ( storedCredentials ? . DefaultProvider )
124183 ? null
125184 : storedCredentials ! . DefaultProvider ;
@@ -128,11 +187,35 @@ public static async Task<int> Main(string[] args)
128187 ! string . IsNullOrWhiteSpace ( storedCredentials ? . DefaultModel )
129188 ? storedCredentials ! . DefaultModel
130189 : null ;
131- var sessionTarget = AiProviderSelection . ResolveSessionTarget (
132- options . Provider ?? defaultProviderFromStore ,
133- options . Model ?? defaultModelFromStore ,
134- resumed ? . SourceSession . Provider ,
135- resumed ? . Model ) ;
190+
191+ // An explicit --provider / --model flag always beats a stored proxy
192+ // default: the user asked for something specific, so don't silently
193+ // reroute them through an AI Service. Absent overrides, the proxy
194+ // slug forces OpenAI-compat routing via /api/v1/proxy/s/{slug}/v1/.
195+ var activeProxySlug = string . IsNullOrWhiteSpace ( options . Provider ) &&
196+ ! string . IsNullOrWhiteSpace ( storedCredentials ? . DefaultProxySlug )
197+ ? storedCredentials ! . DefaultProxySlug
198+ : null ;
199+ var activeProxyLabel = activeProxySlug != null
200+ ? storedCredentials ? . DefaultProxyLabel
201+ : null ;
202+
203+ AiSessionTarget sessionTarget ;
204+ if ( activeProxySlug != null )
205+ {
206+ sessionTarget = AiProviderSelection . ResolveSessionTargetForProxyService (
207+ options . Model ,
208+ defaultModelFromStore ,
209+ resumed ? . Model ) ;
210+ }
211+ else
212+ {
213+ sessionTarget = AiProviderSelection . ResolveSessionTarget (
214+ options . Provider ?? defaultProviderFromStore ,
215+ options . Model ?? defaultModelFromStore ,
216+ resumed ? . SourceSession . Provider ,
217+ resumed ? . Model ) ;
218+ }
136219 var aiProvider = sessionTarget . Provider ;
137220 var model = sessionTarget . Model ;
138221
@@ -155,7 +238,9 @@ public static async Task<int> Main(string[] args)
155238 : new NyxIdRoutingContext (
156239 nyxIdSettings . ActiveBaseUrl ,
157240 nyxIdTokenProvider ,
158- nyxIdSettings . HasStoredCredentials ) ;
241+ nyxIdSettings . HasStoredCredentials ,
242+ ProxyServiceSlug : activeProxySlug ,
243+ ProxyServiceLabel : activeProxyLabel ) ;
159244 using var chatClientRuntime = ChatClientRuntime . Create (
160245 aiProvider ,
161246 model ,
@@ -264,12 +349,14 @@ await journal.SeedAsync(
264349 agentRuntimeOptions ,
265350 agentSettings . Settings . BackgroundRunConcurrency ) ;
266351 var nyxIdLlmStatusClient = new NyxIdLlmStatusClient ( nyxIdTokenProvider ) ;
352+ var nyxIdKeysClient = new NyxIdKeysClient ( nyxIdTokenProvider ) ;
267353 var aevatarSettingsStore = new AevatarChatSettingsStore ( ) ;
268354 var commandRegistry = BuildCommandRegistry (
269355 skillLoader . Load ( workingDirectory ) ,
270356 nyxIdAuthService ,
271357 nyxIdCredentialStore ,
272358 nyxIdLlmStatusClient ,
359+ nyxIdKeysClient ,
273360 nyxIdSettings . ActiveBaseUrl ,
274361 nyxIdTokenProvider ,
275362 aevatarSettingsStore ,
@@ -740,6 +827,7 @@ private static CommandRegistry BuildCommandRegistry(
740827 NyxIdAuthService nyxIdAuthService ,
741828 NyxIdCredentialStore nyxIdCredentialStore ,
742829 NyxIdLlmStatusClient nyxIdLlmStatusClient ,
830+ NyxIdKeysClient nyxIdKeysClient ,
743831 string nyxIdBaseUrl ,
744832 NyxIdTokenProvider nyxIdTokenProvider ,
745833 AevatarChatSettingsStore aevatarSettingsStore ,
@@ -767,7 +855,7 @@ private static CommandRegistry BuildCommandRegistry(
767855 registry . Register ( new FastCommand ( ) ) ;
768856 registry . Register ( new HelpCommand ( ) ) ;
769857 registry . Register ( new InitCommand ( ) ) ;
770- registry . Register ( new LlmCommand ( nyxIdCredentialStore , nyxIdLlmStatusClient ) ) ;
858+ registry . Register ( new LlmCommand ( nyxIdCredentialStore , nyxIdLlmStatusClient , nyxIdKeysClient ) ) ;
771859 registry . Register ( new LoginCommand ( nyxIdAuthService , nyxIdCredentialStore , nyxIdBaseUrl ) ) ;
772860 registry . Register ( new LogoutCommand ( nyxIdAuthService , nyxIdCredentialStore ) ) ;
773861 registry . Register ( new MailboxCommand ( ) ) ;
@@ -849,6 +937,31 @@ AEXON_CHAT_LOGGING Enable MEAI pipeline logging on the console (1/true)
849937 "storage" ,
850938 } ;
851939
940+ /// <summary>
941+ /// Returns true when <paramref name="initialPrompt"/> dispatches one of
942+ /// the setup-fixing subcommands (<c>/login</c>, <c>/logout</c>,
943+ /// <c>/llm</c>, <c>/aevatar</c>, <c>/storage</c>). The onboarding gate
944+ /// bails out in that case — we must let users reach those commands even
945+ /// if they haven't signed in yet, otherwise they'd have no way to fix
946+ /// their setup.
947+ /// </summary>
948+ private static bool IsSetupFixingSubcommand ( string ? initialPrompt )
949+ {
950+ if ( string . IsNullOrWhiteSpace ( initialPrompt ) )
951+ return false ;
952+
953+ var trimmed = initialPrompt . TrimStart ( ) ;
954+ if ( trimmed . Length == 0 || trimmed [ 0 ] != '/' )
955+ return false ;
956+
957+ var end = 1 ;
958+ while ( end < trimmed . Length && trimmed [ end ] != ' ' && trimmed [ end ] != '\t ' )
959+ end ++ ;
960+
961+ var head = trimmed [ 1 ..end ] ;
962+ return TopLevelSubcommands . Contains ( head ) ;
963+ }
964+
852965 /// <summary>
853966 /// Lets users invoke slash commands without the leading slash as a top-level
854967 /// subcommand (`aexon login`, `aexon llm use anthropic`). Only the first
0 commit comments