1717import java .util .List ;
1818import java .util .Map ;
1919import java .util .Objects ;
20- import java .util .concurrent .ExecutorService ;
21- import java .util .concurrent .LinkedBlockingQueue ;
22- import java .util .concurrent .RejectedExecutionException ;
23- import java .util .concurrent .ThreadPoolExecutor ;
24- import java .util .concurrent .TimeUnit ;
25- import java .util .concurrent .atomic .AtomicInteger ;
20+ import java .util .concurrent .CompletableFuture ;
2621import java .util .function .Consumer ;
2722
2823/**
@@ -65,7 +60,6 @@ public class OpenCodeClient implements AutoCloseable {
6560 private final OpenCodeChatClient chatClient ;
6661 private final OpenCodeSseClient sseClient ;
6762 private final OpenCodeCli cli ;
68- private final ExecutorService streamExecutor ;
6963
7064 // ============================================================
7165 // 构造器
@@ -128,7 +122,6 @@ public OpenCodeClient(OpenCodeHttpClientConfig httpConfig, OpenCodeCliConfig cli
128122 this .chatClient = null ;
129123 this .sseClient = null ;
130124 }
131- this .streamExecutor = createStreamExecutor (httpConfig );
132125
133126 // CLI 子系统初始化
134127 if (cliEnabled ) {
@@ -167,21 +160,6 @@ public OpenCodeClient(OpenCodeClientConfig config,
167160 this .chatClient = httpClient instanceof OpenCodeChatClient ? (OpenCodeChatClient ) httpClient : null ;
168161 this .sseClient = sseClient ;
169162 this .cli = cli ;
170- this .streamExecutor = createStreamExecutor (config .getHttp ());
171- }
172-
173- private static ExecutorService createStreamExecutor (OpenCodeHttpClientConfig config ) {
174- int corePoolSize = Math .max (1 , config .getStreamCorePoolSize ());
175- int maxPoolSize = Math .max (corePoolSize , config .getStreamMaxPoolSize ());
176- AtomicInteger threadIndex = new AtomicInteger ();
177- return new ThreadPoolExecutor (corePoolSize , maxPoolSize ,
178- Math .max (1L , config .getStreamKeepAliveMillis ()), TimeUnit .MILLISECONDS ,
179- new LinkedBlockingQueue <>(Math .max (1 , config .getStreamQueueCapacity ())), runnable -> {
180- Thread thread = new Thread (runnable ,
181- "opencode-stream-consumer-" + threadIndex .incrementAndGet ());
182- thread .setDaemon (true );
183- return thread ;
184- }, new ThreadPoolExecutor .AbortPolicy ());
185163 }
186164
187165 // ============================================================
@@ -341,7 +319,7 @@ public PromptResult chatCompletionWithSession(String text, String providerID, St
341319 return httpClient .chatCompletionWithSession (PromptRequest .ofText (text , providerID , modelID ), sessionKey );
342320 }
343321
344- public boolean chatCompletionWithSessionAsync (PromptRequest request , String sessionKey ) {
322+ public CompletableFuture < Boolean > chatCompletionWithSessionAsync (PromptRequest request , String sessionKey ) {
345323 return httpClient .chatCompletionWithSessionAsync (request , sessionKey );
346324 }
347325
@@ -355,6 +333,11 @@ public ChatResponse chatCompletion(String sessionId, ChatRequest request) {
355333 return ChatMessageMapper .toChatResponse (result );
356334 }
357335
336+ /** 异步完成指定会话的聊天请求。 */
337+ public CompletableFuture <ChatResponse > chatCompletionAsync (String sessionId , ChatRequest request ) {
338+ return chatClient .chatCompletionAsync (sessionId , request );
339+ }
340+
358341 public ChatResponse chatCompletionWithSession (ChatRequest request , String sessionKey ) {
359342 PromptRequest promptRequest = ChatMessageMapper .toPromptRequest (request );
360343 PromptResult result = httpClient .chatCompletionWithSession (promptRequest , sessionKey );
@@ -368,6 +351,13 @@ public ChatResponse chatCompletionWithSession(ChatRequest request, String sessio
368351 return ChatMessageMapper .toChatResponse (result );
369352 }
370353
354+ /** 异步查找会话并完成聊天请求。 */
355+ public CompletableFuture <ChatResponse > chatCompletionWithSessionAsync (ChatRequest request ,
356+ String sessionKey ,
357+ HttpCallCancellation cancellation ) {
358+ return chatClient .chatCompletionWithSessionAsync (request , sessionKey , cancellation );
359+ }
360+
371361 public StreamingChatResponse chatCompletionStream (ChatRequest request , String sessionKey ) {
372362 return chatCompletionStream (request , sessionKey , null );
373363 }
@@ -383,117 +373,23 @@ public StreamingChatResponse chatCompletionStream(ChatRequest request, String se
383373 public StreamingChatResponse chatCompletionStream (ChatRequest request , String sessionKey ,
384374 OpenCodeRequestContext context ,
385375 Consumer <String > deltaConsumer ) {
386- String sessionId = httpClient .ensureSession (sessionKey , context );
387- PromptRequest promptRequest = ChatMessageMapper .toPromptRequest (request );
388-
389- StreamingChatResponse stream = new StreamingChatResponse ().onDelta (deltaConsumer );
390-
391- OpenCodeSseClient .QueueSubscription subscription =
392- sseClient .subscribeQueueSubscription (context );
393- java .util .concurrent .BlockingQueue <Event > queue = subscription .getQueue ();
394-
395- try {
396- streamExecutor .submit (() -> {
397- try {
398- long deadline = System .currentTimeMillis () + (config .getCli ().getTimeout () * 1000L );
399- while (!stream .isDone () && System .currentTimeMillis () < deadline ) {
400- Event event = queue .poll (3 , java .util .concurrent .TimeUnit .SECONDS );
401- if (event == null ) {
402- continue ;
403- }
404-
405- String eventSessionId = event .getProperties () != null
406- ? Objects .toString (event .getProperties ().get ("sessionID" ), null ) : null ;
407- if (eventSessionId == null || !eventSessionId .equals (sessionId )) {
408- continue ;
409- }
410-
411- String type = event .getType ();
412- if (type == null ) {
413- continue ;
414- }
415-
416- if (type .contains ("text.delta" ) || type .contains ("message.part.updated" )) {
417- String delta = extractDeltaText (event );
418- if (delta != null && !delta .isEmpty ()) {
419- stream .acceptDelta (delta );
420- }
421- }
422-
423- if (type .contains ("session.status" ) || type .contains ("session.idle" )) {
424- String status = event .getProperties () != null
425- ? Objects .toString (event .getProperties ().get ("status" ), null ) : null ;
426- if ("idle" .equals (status ) || type .contains ("idle" )) {
427- stream .finish ();
428- return ;
429- }
430- }
431-
432- if (type .contains ("session.error" )) {
433- String error = event .getProperties () != null
434- ? Objects .toString (event .getProperties ().get ("error" ), "unknown error" ) : "unknown error" ;
435- stream .fail (new RuntimeException (error ));
436- return ;
437- }
438- }
439- if (!stream .isDone ()) {
440- stream .fail (new RuntimeException ("Stream timed out for session: " + sessionId ));
441- }
442- } catch (InterruptedException e ) {
443- Thread .currentThread ().interrupt ();
444- stream .fail (e );
445- } catch (Exception e ) {
446- stream .fail (e );
447- } finally {
448- subscription .close ();
449- }
450- });
451- } catch (RejectedExecutionException error ) {
452- subscription .close ();
453- stream .fail (new IllegalStateException ("OpenCode stream executor is full" , error ));
376+ if (Objects .isNull (chatClient )) {
377+ StreamingChatResponse stream = new StreamingChatResponse ();
378+ stream .fail (new IllegalStateException ("OpenCodeChatClient is not configured" ));
454379 return stream ;
455380 }
456-
457- try {
458- if (!httpClient .promptAsync (sessionId , promptRequest , context )) {
459- subscription .close ();
460- stream .fail (new IllegalStateException ("OpenCode async prompt was rejected" ));
461- }
462- } catch (RuntimeException error ) {
463- subscription .close ();
464- stream .fail (error );
465- }
466-
467- return stream ;
468- }
469-
470- private static String extractDeltaText (Event event ) {
471- if (event .getProperties () == null ) {
472- return null ;
473- }
474- Object part = event .getProperties ().get ("part" );
475- if (part instanceof Map ) {
476- Object text = ((Map <?, ?>) part ).get ("text" );
477- if (text != null ) {
478- return text .toString ();
479- }
480- }
481- Object delta = event .getProperties ().get ("delta" );
482- if (delta != null ) {
483- return delta .toString ();
484- }
485- return null ;
381+ return chatClient .chatCompletionStream (request , sessionKey , context , deltaConsumer );
486382 }
487383
488384 public String ensureSession (String sessionKey ) {
489385 return httpClient .ensureSession (sessionKey );
490386 }
491387
492- public boolean chatCompletionAsync (String sessionId , PromptRequest request ) {
388+ public CompletableFuture < Boolean > chatCompletionAsync (String sessionId , PromptRequest request ) {
493389 return httpClient .promptAsync (sessionId , request );
494390 }
495391
496- public boolean chatCompletionAsync (String sessionId , String text ) {
392+ public CompletableFuture < Boolean > chatCompletionAsync (String sessionId , String text ) {
497393 return httpClient .promptAsync (sessionId , PromptRequest .ofText (text ));
498394 }
499395
@@ -521,6 +417,11 @@ public HealthStatus health() {
521417 return httpClient .health ();
522418 }
523419
420+ /** 异步检查 OpenCode Server 健康状态。 */
421+ public CompletableFuture <HealthStatus > healthAsync () {
422+ return httpClient .healthAsync ();
423+ }
424+
524425 // ============================================================
525426 // SSE 事件流
526427 // ============================================================
@@ -976,7 +877,6 @@ public io.github.easy4j.opencode.cli.OpenCodeCliResult cliPr(int number) {
976877
977878 @ Override
978879 public void close () {
979- if (streamExecutor != null ) streamExecutor .shutdownNow ();
980880 if (httpClient != null ) httpClient .close ();
981881 if (sseClient != null ) sseClient .close ();
982882 }
0 commit comments