@@ -89,6 +89,8 @@ public class CopilotClient implements AutoCloseable {
8989 private volatile boolean disposed = false ;
9090 private final String optionsHost ;
9191 private final Integer optionsPort ;
92+ private volatile List <ModelInfo > modelsCache ;
93+ private final Object modelsCacheLock = new Object ();
9294
9395 /**
9496 * Creates a new CopilotClient with default options.
@@ -114,6 +116,13 @@ public CopilotClient(CopilotClientOptions options) {
114116 throw new IllegalArgumentException ("CliUrl is mutually exclusive with UseStdio and CliPath" );
115117 }
116118
119+ // Validate auth options with external server
120+ if (this .options .getCliUrl () != null && !this .options .getCliUrl ().isEmpty ()
121+ && (this .options .getGithubToken () != null || this .options .getUseLoggedInUser () != null )) {
122+ throw new IllegalArgumentException (
123+ "GithubToken and UseLoggedInUser cannot be used with CliUrl (external server manages its own auth)" );
124+ }
125+
117126 // Parse CliUrl if provided
118127 if (this .options .getCliUrl () != null && !this .options .getCliUrl ().isEmpty ()) {
119128 URI uri = parseCliUrl (this .options .getCliUrl ());
@@ -217,6 +226,16 @@ private void registerRpcHandlers(JsonRpcClient rpc) {
217226 rpc .registerMethodHandler ("permission.request" , (requestId , params ) -> {
218227 handlePermissionRequest (rpc , requestId , params );
219228 });
229+
230+ // Handle user input requests
231+ rpc .registerMethodHandler ("userInput.request" , (requestId , params ) -> {
232+ handleUserInputRequest (rpc , requestId , params );
233+ });
234+
235+ // Handle hooks invocations
236+ rpc .registerMethodHandler ("hooks.invoke" , (requestId , params ) -> {
237+ handleHooksInvoke (rpc , requestId , params );
238+ });
220239 }
221240
222241 private void handleToolCall (JsonRpcClient rpc , String requestId , JsonNode params ) {
@@ -317,6 +336,93 @@ private void handlePermissionRequest(JsonRpcClient rpc, String requestId, JsonNo
317336 });
318337 }
319338
339+ private void handleUserInputRequest (JsonRpcClient rpc , String requestId , JsonNode params ) {
340+ CompletableFuture .runAsync (() -> {
341+ try {
342+ String sessionId = params .get ("sessionId" ).asText ();
343+ String question = params .get ("question" ).asText ();
344+ JsonNode choicesNode = params .get ("choices" );
345+ JsonNode allowFreeformNode = params .get ("allowFreeform" );
346+
347+ CopilotSession session = sessions .get (sessionId );
348+ if (session == null ) {
349+ rpc .sendErrorResponse (Long .parseLong (requestId ), -32602 , "Unknown session " + sessionId );
350+ return ;
351+ }
352+
353+ com .github .copilot .sdk .json .UserInputRequest request = new com .github .copilot .sdk .json .UserInputRequest ()
354+ .setQuestion (question );
355+ if (choicesNode != null && choicesNode .isArray ()) {
356+ List <String > choices = new ArrayList <>();
357+ for (JsonNode choice : choicesNode ) {
358+ choices .add (choice .asText ());
359+ }
360+ request .setChoices (choices );
361+ }
362+ if (allowFreeformNode != null ) {
363+ request .setAllowFreeform (allowFreeformNode .asBoolean ());
364+ }
365+
366+ session .handleUserInputRequest (request ).thenAccept (response -> {
367+ try {
368+ rpc .sendResponse (Long .parseLong (requestId ),
369+ Map .of ("answer" , response .getAnswer (), "wasFreeform" , response .isWasFreeform ()));
370+ } catch (IOException e ) {
371+ LOG .log (Level .SEVERE , "Error sending user input response" , e );
372+ }
373+ }).exceptionally (ex -> {
374+ try {
375+ rpc .sendErrorResponse (Long .parseLong (requestId ), -32603 ,
376+ "User input handler error: " + ex .getMessage ());
377+ } catch (IOException e ) {
378+ LOG .log (Level .SEVERE , "Error sending user input error" , e );
379+ }
380+ return null ;
381+ });
382+ } catch (Exception e ) {
383+ LOG .log (Level .SEVERE , "Error handling user input request" , e );
384+ }
385+ });
386+ }
387+
388+ private void handleHooksInvoke (JsonRpcClient rpc , String requestId , JsonNode params ) {
389+ CompletableFuture .runAsync (() -> {
390+ try {
391+ String sessionId = params .get ("sessionId" ).asText ();
392+ String hookType = params .get ("hookType" ).asText ();
393+ JsonNode input = params .get ("input" );
394+
395+ CopilotSession session = sessions .get (sessionId );
396+ if (session == null ) {
397+ rpc .sendErrorResponse (Long .parseLong (requestId ), -32602 , "Unknown session " + sessionId );
398+ return ;
399+ }
400+
401+ session .handleHooksInvoke (hookType , input ).thenAccept (output -> {
402+ try {
403+ if (output != null ) {
404+ rpc .sendResponse (Long .parseLong (requestId ), Map .of ("output" , output ));
405+ } else {
406+ rpc .sendResponse (Long .parseLong (requestId ), Map .of ("output" , (Object ) null ));
407+ }
408+ } catch (IOException e ) {
409+ LOG .log (Level .SEVERE , "Error sending hooks response" , e );
410+ }
411+ }).exceptionally (ex -> {
412+ try {
413+ rpc .sendErrorResponse (Long .parseLong (requestId ), -32603 ,
414+ "Hooks handler error: " + ex .getMessage ());
415+ } catch (IOException e ) {
416+ LOG .log (Level .SEVERE , "Error sending hooks error" , e );
417+ }
418+ return null ;
419+ });
420+ } catch (Exception e ) {
421+ LOG .log (Level .SEVERE , "Error handling hooks invoke" , e );
422+ }
423+ });
424+ }
425+
320426 private void verifyProtocolVersion (Connection connection ) throws Exception {
321427 int expectedVersion = SdkProtocolVersion .get ();
322428 Map <String , Object > params = new HashMap <>();
@@ -373,6 +479,9 @@ private CompletableFuture<Void> cleanupConnection() {
373479 CompletableFuture <Connection > future = connectionFuture ;
374480 connectionFuture = null ;
375481
482+ // Clear models cache
483+ modelsCache = null ;
484+
376485 if (future == null ) {
377486 return CompletableFuture .completedFuture (null );
378487 }
@@ -414,6 +523,7 @@ public CompletableFuture<CopilotSession> createSession(SessionConfig config) {
414523 if (config != null ) {
415524 request .setModel (config .getModel ());
416525 request .setSessionId (config .getSessionId ());
526+ request .setReasoningEffort (config .getReasoningEffort ());
417527 request .setTools (config .getTools () != null
418528 ? config .getTools ().stream ()
419529 .map (t -> new ToolDef (t .getName (), t .getDescription (), t .getParameters ()))
@@ -424,6 +534,9 @@ public CompletableFuture<CopilotSession> createSession(SessionConfig config) {
424534 request .setExcludedTools (config .getExcludedTools ());
425535 request .setProvider (config .getProvider ());
426536 request .setRequestPermission (config .getOnPermissionRequest () != null ? true : null );
537+ request .setRequestUserInput (config .getOnUserInputRequest () != null ? true : null );
538+ request .setHooks (config .getHooks () != null && config .getHooks ().hasHooks () ? true : null );
539+ request .setWorkingDirectory (config .getWorkingDirectory ());
427540 request .setStreaming (config .isStreaming () ? true : null );
428541 request .setMcpServers (config .getMcpServers ());
429542 request .setCustomAgents (config .getCustomAgents ());
@@ -442,6 +555,12 @@ public CompletableFuture<CopilotSession> createSession(SessionConfig config) {
442555 if (config != null && config .getOnPermissionRequest () != null ) {
443556 session .registerPermissionHandler (config .getOnPermissionRequest ());
444557 }
558+ if (config != null && config .getOnUserInputRequest () != null ) {
559+ session .registerUserInputHandler (config .getOnUserInputRequest ());
560+ }
561+ if (config != null && config .getHooks () != null ) {
562+ session .registerHooks (config .getHooks ());
563+ }
445564 sessions .put (response .getSessionId (), session );
446565 return session ;
447566 });
@@ -478,13 +597,18 @@ public CompletableFuture<CopilotSession> resumeSession(String sessionId, ResumeS
478597 ResumeSessionRequest request = new ResumeSessionRequest ();
479598 request .setSessionId (sessionId );
480599 if (config != null ) {
600+ request .setReasoningEffort (config .getReasoningEffort ());
481601 request .setTools (config .getTools () != null
482602 ? config .getTools ().stream ()
483603 .map (t -> new ToolDef (t .getName (), t .getDescription (), t .getParameters ()))
484604 .collect (Collectors .toList ())
485605 : null );
486606 request .setProvider (config .getProvider ());
487607 request .setRequestPermission (config .getOnPermissionRequest () != null ? true : null );
608+ request .setRequestUserInput (config .getOnUserInputRequest () != null ? true : null );
609+ request .setHooks (config .getHooks () != null && config .getHooks ().hasHooks () ? true : null );
610+ request .setWorkingDirectory (config .getWorkingDirectory ());
611+ request .setDisableResume (config .isDisableResume () ? true : null );
488612 request .setStreaming (config .isStreaming () ? true : null );
489613 request .setMcpServers (config .getMcpServers ());
490614 request .setCustomAgents (config .getCustomAgents ());
@@ -501,6 +625,12 @@ public CompletableFuture<CopilotSession> resumeSession(String sessionId, ResumeS
501625 if (config != null && config .getOnPermissionRequest () != null ) {
502626 session .registerPermissionHandler (config .getOnPermissionRequest ());
503627 }
628+ if (config != null && config .getOnUserInputRequest () != null ) {
629+ session .registerUserInputHandler (config .getOnUserInputRequest ());
630+ }
631+ if (config != null && config .getHooks () != null ) {
632+ session .registerHooks (config .getHooks ());
633+ }
504634 sessions .put (response .getSessionId (), session );
505635 return session ;
506636 });
@@ -576,13 +706,36 @@ public CompletableFuture<GetAuthStatusResponse> getAuthStatus() {
576706
577707 /**
578708 * Lists available models with their metadata.
709+ * <p>
710+ * Results are cached after the first successful call to avoid rate limiting.
711+ * The cache is cleared when the client disconnects.
579712 *
580713 * @return a future that resolves with a list of available models
581714 * @see ModelInfo
582715 */
583716 public CompletableFuture <List <ModelInfo >> listModels () {
584- return ensureConnected ().thenCompose (connection -> connection .rpc
585- .invoke ("models.list" , Map .of (), GetModelsResponse .class ).thenApply (GetModelsResponse ::getModels ));
717+ // Check cache first
718+ List <ModelInfo > cached = modelsCache ;
719+ if (cached != null ) {
720+ return CompletableFuture .completedFuture (new ArrayList <>(cached ));
721+ }
722+
723+ return ensureConnected ().thenCompose (connection -> {
724+ // Double-check cache inside lock
725+ synchronized (modelsCacheLock ) {
726+ if (modelsCache != null ) {
727+ return CompletableFuture .completedFuture (new ArrayList <>(modelsCache ));
728+ }
729+ }
730+
731+ return connection .rpc .invoke ("models.list" , Map .of (), GetModelsResponse .class ).thenApply (response -> {
732+ List <ModelInfo > models = response .getModels ();
733+ synchronized (modelsCacheLock ) {
734+ modelsCache = models ;
735+ }
736+ return new ArrayList <>(models ); // Return a copy to prevent cache mutation
737+ });
738+ });
586739 }
587740
588741 /**
@@ -668,6 +821,20 @@ private ProcessInfo startCliServer() throws IOException, InterruptedException {
668821 args .add (String .valueOf (options .getPort ()));
669822 }
670823
824+ // Add auth-related flags
825+ if (options .getGithubToken () != null && !options .getGithubToken ().isEmpty ()) {
826+ args .add ("--auth-token-env" );
827+ args .add ("COPILOT_SDK_AUTH_TOKEN" );
828+ }
829+
830+ // Default UseLoggedInUser to false when GithubToken is provided
831+ boolean useLoggedInUser = options .getUseLoggedInUser () != null
832+ ? options .getUseLoggedInUser ()
833+ : (options .getGithubToken () == null || options .getGithubToken ().isEmpty ());
834+ if (!useLoggedInUser ) {
835+ args .add ("--no-auto-login" );
836+ }
837+
671838 List <String > command = resolveCliCommand (cliPath , args );
672839
673840 ProcessBuilder pb = new ProcessBuilder (command );
@@ -683,6 +850,11 @@ private ProcessInfo startCliServer() throws IOException, InterruptedException {
683850 }
684851 pb .environment ().remove ("NODE_DEBUG" );
685852
853+ // Set auth token in environment if provided
854+ if (options .getGithubToken () != null && !options .getGithubToken ().isEmpty ()) {
855+ pb .environment ().put ("COPILOT_SDK_AUTH_TOKEN" , options .getGithubToken ());
856+ }
857+
686858 Process process = pb .start ();
687859
688860 // Forward stderr to logger in background
0 commit comments