1515import java .util .List ;
1616import java .util .Map ;
1717import java .util .Objects ;
18+ import java .util .concurrent .ExecutorService ;
19+ import java .util .concurrent .LinkedBlockingQueue ;
20+ import java .util .concurrent .RejectedExecutionException ;
21+ import java .util .concurrent .ThreadPoolExecutor ;
22+ import java .util .concurrent .TimeUnit ;
23+ import java .util .concurrent .atomic .AtomicInteger ;
1824
1925/**
2026 * OpenCode 客户端门面:HTTP Server + SSE 事件流 + 本地 CLI。
@@ -48,14 +54,15 @@ public class OpenCodeClient implements AutoCloseable {
4854 private final OpenCodeHttpClient httpClient ;
4955 private final OpenCodeSseClient sseClient ;
5056 private final OpenCodeCli cli ;
57+ private final ExecutorService streamExecutor ;
5158
5259 // ============================================================
5360 // 构造器
5461 // ============================================================
5562
5663 /** 仅 HTTP 子系统(CLI 禁用)。自动创建默认 ObjectMapper 与 OkHttpClient。 */
5764 public OpenCodeClient (OpenCodeHttpClientConfig httpConfig ) {
58- this (httpConfig , new OpenCodeCliConfig (), new ObjectMapper (), new OkHttpClient () );
65+ this (httpConfig , new OpenCodeCliConfig (), new ObjectMapper (), null );
5966 }
6067
6168 /** 仅 HTTP 子系统(CLI 禁用),强制注入共享 ObjectMapper 与 OkHttpClient。 */
@@ -65,7 +72,7 @@ public OpenCodeClient(OpenCodeHttpClientConfig httpConfig, ObjectMapper objectMa
6572
6673 /** 仅 CLI 子系统(HTTP 禁用)。自动创建默认 ObjectMapper 与 OkHttpClient。 */
6774 public OpenCodeClient (OpenCodeCliConfig cliConfig ) {
68- this (new OpenCodeHttpClientConfig (), cliConfig , new ObjectMapper (), new OkHttpClient () );
75+ this (new OpenCodeHttpClientConfig (), cliConfig , new ObjectMapper (), null );
6976 }
7077
7178 /** 仅 CLI 子系统(HTTP 禁用),强制注入共享 ObjectMapper 与 OkHttpClient。 */
@@ -75,7 +82,7 @@ public OpenCodeClient(OpenCodeCliConfig cliConfig, ObjectMapper objectMapper, Ok
7582
7683 /** HTTP + CLI 子系统。自动创建默认 ObjectMapper 与 OkHttpClient。 */
7784 public OpenCodeClient (OpenCodeHttpClientConfig httpConfig , OpenCodeCliConfig cliConfig ) {
78- this (httpConfig , cliConfig , new ObjectMapper (), new OkHttpClient () );
85+ this (httpConfig , cliConfig , new ObjectMapper (), null );
7986 }
8087
8188 /**
@@ -91,7 +98,6 @@ public OpenCodeClient(OpenCodeHttpClientConfig httpConfig, OpenCodeCliConfig cli
9198 Objects .requireNonNull (httpConfig , "httpConfig" );
9299 Objects .requireNonNull (cliConfig , "cliConfig" );
93100 Objects .requireNonNull (objectMapper , "objectMapper" );
94- Objects .requireNonNull (httpClient , "httpClient" );
95101
96102 boolean httpEnabled = httpConfig .isEnabled ();
97103 boolean cliEnabled = cliConfig .isEnabled ();
@@ -105,11 +111,12 @@ public OpenCodeClient(OpenCodeHttpClientConfig httpConfig, OpenCodeCliConfig cli
105111 if (httpEnabled ) {
106112 this .httpClient = new OpenCodeHttpClient (httpConfig , objectMapper , httpClient );
107113 this .sseClient = new OpenCodeSseClient (httpConfig , objectMapper ,
108- httpClient != null ? httpClient : this .httpClient .getOkHttpClient ());
114+ this .httpClient .getOkHttpClient ());
109115 } else {
110116 this .httpClient = null ;
111117 this .sseClient = null ;
112118 }
119+ this .streamExecutor = createStreamExecutor (httpConfig );
113120
114121 // CLI 子系统初始化
115122 if (cliEnabled ) {
@@ -124,7 +131,7 @@ public OpenCodeClient(OpenCodeHttpClientConfig httpConfig, OpenCodeCliConfig cli
124131
125132 /** 组合配置,自动创建默认 ObjectMapper 与 OkHttpClient。 */
126133 public OpenCodeClient (OpenCodeClientConfig config ) {
127- this (config , new ObjectMapper (), new OkHttpClient () );
134+ this (config , new ObjectMapper (), null );
128135 }
129136
130137 /** 组合配置,强制注入共享 ObjectMapper 与 OkHttpClient。 */
@@ -147,6 +154,21 @@ public OpenCodeClient(OpenCodeClientConfig config,
147154 this .httpClient = httpClient ;
148155 this .sseClient = sseClient ;
149156 this .cli = cli ;
157+ this .streamExecutor = createStreamExecutor (config .getHttp ());
158+ }
159+
160+ private static ExecutorService createStreamExecutor (OpenCodeHttpClientConfig config ) {
161+ int corePoolSize = Math .max (1 , config .getStreamCorePoolSize ());
162+ int maxPoolSize = Math .max (corePoolSize , config .getStreamMaxPoolSize ());
163+ AtomicInteger threadIndex = new AtomicInteger ();
164+ return new ThreadPoolExecutor (corePoolSize , maxPoolSize ,
165+ Math .max (1L , config .getStreamKeepAliveMillis ()), TimeUnit .MILLISECONDS ,
166+ new LinkedBlockingQueue <>(Math .max (1 , config .getStreamQueueCapacity ())), runnable -> {
167+ Thread thread = new Thread (runnable ,
168+ "opencode-stream-consumer-" + threadIndex .incrementAndGet ());
169+ thread .setDaemon (true );
170+ return thread ;
171+ }, new ThreadPoolExecutor .AbortPolicy ());
150172 }
151173
152174 // ============================================================
@@ -218,6 +240,11 @@ private void copyHttpConfig(OpenCodeHttpClientConfig src) {
218240 this .config .getHttp ().setKeepAliveDurationMillis (src .getKeepAliveDurationMillis ());
219241 this .config .getHttp ().setMaxRequests (src .getMaxRequests ());
220242 this .config .getHttp ().setMaxRequestsPerHost (src .getMaxRequestsPerHost ());
243+ this .config .getHttp ().setStreamCorePoolSize (src .getStreamCorePoolSize ());
244+ this .config .getHttp ().setStreamMaxPoolSize (src .getStreamMaxPoolSize ());
245+ this .config .getHttp ().setStreamQueueCapacity (src .getStreamQueueCapacity ());
246+ this .config .getHttp ().setStreamKeepAliveMillis (src .getStreamKeepAliveMillis ());
247+ this .config .getHttp ().setSseEventQueueCapacity (src .getSseEventQueueCapacity ());
221248 this .config .getHttp ().setRetryOnConnectionFailure (src .isRetryOnConnectionFailure ());
222249 this .config .getHttp ().setVerifySsl (src .isVerifySsl ());
223250 this .config .getHttp ().setDefaultModel (src .getDefaultModel ());
@@ -287,6 +314,11 @@ public PromptResult chatCompletionWithSession(PromptRequest request, String sess
287314 return httpClient .chatCompletionWithSession (request , sessionKey );
288315 }
289316
317+ public PromptResult chatCompletionWithSession (PromptRequest request , String sessionKey ,
318+ HttpCallCancellation cancellation ) {
319+ return httpClient .chatCompletionWithSession (request , sessionKey , cancellation );
320+ }
321+
290322 public PromptResult chatCompletionWithSession (String text , String sessionKey ) {
291323 return httpClient .chatCompletionWithSession (PromptRequest .ofText (text ), sessionKey );
292324 }
@@ -315,6 +347,13 @@ public ChatResponse chatCompletionWithSession(ChatRequest request, String sessio
315347 return ChatMessageMapper .toChatResponse (result );
316348 }
317349
350+ public ChatResponse chatCompletionWithSession (ChatRequest request , String sessionKey ,
351+ HttpCallCancellation cancellation ) {
352+ PromptRequest promptRequest = ChatMessageMapper .toPromptRequest (request );
353+ PromptResult result = httpClient .chatCompletionWithSession (promptRequest , sessionKey , cancellation );
354+ return ChatMessageMapper .toChatResponse (result );
355+ }
356+
318357 public ChatStreamingResponse chatCompletionStream (ChatRequest request , String sessionKey ) {
319358 return chatCompletionStream (request , sessionKey , null );
320359 }
@@ -326,9 +365,12 @@ public ChatStreamingResponse chatCompletionStream(ChatRequest request, String se
326365
327366 ChatStreamingResponse stream = new ChatStreamingResponse ();
328367
329- java .util .concurrent .BlockingQueue <Event > queue = sseClient .subscribeQueue (context );
368+ OpenCodeSseClient .QueueSubscription subscription =
369+ sseClient .subscribeQueueSubscription (context );
370+ java .util .concurrent .BlockingQueue <Event > queue = subscription .getQueue ();
330371
331- java .util .concurrent .CompletableFuture .runAsync (() -> {
372+ try {
373+ streamExecutor .submit (() -> {
332374 try {
333375 long deadline = System .currentTimeMillis () + (config .getCli ().getTimeout () * 1000L );
334376 while (!stream .isDone () && System .currentTimeMillis () < deadline ) {
@@ -379,10 +421,25 @@ public ChatStreamingResponse chatCompletionStream(ChatRequest request, String se
379421 stream .fail (e );
380422 } catch (Exception e ) {
381423 stream .fail (e );
424+ } finally {
425+ subscription .close ();
382426 }
383- });
427+ });
428+ } catch (RejectedExecutionException error ) {
429+ subscription .close ();
430+ stream .fail (new IllegalStateException ("OpenCode stream executor is full" , error ));
431+ return stream ;
432+ }
384433
385- httpClient .promptAsync (sessionId , promptRequest , context );
434+ try {
435+ if (!httpClient .promptAsync (sessionId , promptRequest , context )) {
436+ subscription .close ();
437+ stream .fail (new IllegalStateException ("OpenCode async prompt was rejected" ));
438+ }
439+ } catch (RuntimeException error ) {
440+ subscription .close ();
441+ stream .fail (error );
442+ }
386443
387444 return stream ;
388445 }
@@ -895,6 +952,7 @@ public io.github.easy4j.opencode.cli.OpenCodeCliResult cliPr(int number) {
895952
896953 @ Override
897954 public void close () {
955+ streamExecutor .shutdownNow ();
898956 if (httpClient != null ) httpClient .close ();
899957 if (sseClient != null ) sseClient .close ();
900958 }
0 commit comments