From 88c28d859f975404b6a88140a6171eaba3954255 Mon Sep 17 00:00:00 2001 From: kevin Date: Thu, 4 Jun 2026 10:57:09 +0800 Subject: [PATCH 1/8] code optimization --- samples/ImageSynthesisUsage.java | 11 +- .../aigc/codegeneration/CodeGeneration.java | 16 +- .../aigc/completion/ChatCompletion.java | 1 + .../ChatCompletionStreamOptions.java | 14 +- .../aigc/completion/ChatCompletions.java | 1 + .../dashscope/aigc/generation/Generation.java | 173 ++++--------- .../aigc/generation/GenerationOutput.java | 4 + .../aigc/generation/GenerationParam.java | 32 ++- .../aigc/generation/GenerationUsage.java | 18 +- .../aigc/imagegeneration/ImageGeneration.java | 238 +++++------------- .../aigc/imagesynthesis/ImageSynthesis.java | 13 +- .../MultiModalConversation.java | 227 +++++------------ .../MultiModalConversationOutput.java | 6 + .../MultiModalConversationParam.java | 20 +- .../MultiModalConversationUsage.java | 18 +- .../aigc/videosynthesis/VideoSynthesis.java | 22 +- .../api/SynchronizeHalfDuplexApi.java | 62 +++++ .../alibaba/dashscope/app/Application.java | 13 +- .../dashscope/common/ResponseFormat.java | 32 +++ .../embeddings/BatchTextEmbedding.java | 4 +- .../embeddings/MultiModalEmbedding.java | 18 +- .../dashscope/embeddings/TextEmbedding.java | 19 +- .../multimodal/MultiModalDialog.java | 8 +- .../MultiModalDialogApiKeyWords.java | 1 + .../multimodal/MultiModalDialogCallback.java | 1 + .../multimodal/MultiModalRequestParam.java | 1 + .../alibaba/dashscope/multimodal/State.java | 1 + .../dashscope/multimodal/tingwu/TingWu.java | 39 ++- .../multimodal/tingwu/TingWuParam.java | 1 + .../multimodal/tingwu/TingWuRealtime.java | 7 +- .../tingwu/TingWuRealtimeCallback.java | 1 + .../tingwu/TingWuRealtimeParam.java | 1 + .../nlp/understanding/Understanding.java | 30 ++- .../nlp/understanding/UnderstandingParam.java | 1 + .../alibaba/dashscope/rerank/TextReRank.java | 68 ++++- 35 files changed, 524 insertions(+), 598 deletions(-) diff --git a/samples/ImageSynthesisUsage.java b/samples/ImageSynthesisUsage.java index ae00066d..0ac1db12 100644 --- a/samples/ImageSynthesisUsage.java +++ b/samples/ImageSynthesisUsage.java @@ -7,11 +7,12 @@ import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesisParam; import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesisResult; import com.alibaba.dashscope.exception.ApiException; +import com.alibaba.dashscope.exception.InputRequiredException; import com.alibaba.dashscope.exception.NoApiKeyException; import com.alibaba.dashscope.task.AsyncTaskListParam; public class ImageSynthesisUsage { - public static void basicCall() throws ApiException, NoApiKeyException { + public static void basicCall() throws ApiException, NoApiKeyException, InputRequiredException { // create with image2image, 参考文档(image2image|text2image) ImageSynthesis is = new ImageSynthesis(); ImageSynthesisParam param = @@ -28,7 +29,7 @@ public static void basicCall() throws ApiException, NoApiKeyException { System.out.println(result); } - public static void synCall() throws ApiException, NoApiKeyException { + public static void synCall() throws ApiException, NoApiKeyException, InputRequiredException { ImageSynthesis is = new ImageSynthesis(); ImageSynthesisParam param = ImageSynthesisParam.builder() @@ -43,14 +44,14 @@ public static void synCall() throws ApiException, NoApiKeyException { System.out.println(result); } - public static void listTask() throws ApiException, NoApiKeyException { + public static void listTask() throws ApiException, NoApiKeyException, InputRequiredException { ImageSynthesis is = new ImageSynthesis(); AsyncTaskListParam param = AsyncTaskListParam.builder().build(); ImageSynthesisListResult result = is.list(param); System.out.println(result); } - public void fetchTask() throws ApiException, NoApiKeyException { + public void fetchTask() throws ApiException, NoApiKeyException, InputRequiredException { String taskId = "your task id"; ImageSynthesis is = new ImageSynthesis(); // If set DASHSCOPE_API_KEY environment variable, apiKey can null. @@ -64,7 +65,7 @@ public static void main(String[] args){ // basicCall(); synCall(); //listTask(); - }catch(ApiException|NoApiKeyException e){ + }catch(ApiException|NoApiKeyException|InputRequiredException e){ System.out.println(e.getMessage()); } System.exit(0); diff --git a/src/main/java/com/alibaba/dashscope/aigc/codegeneration/CodeGeneration.java b/src/main/java/com/alibaba/dashscope/aigc/codegeneration/CodeGeneration.java index fb07c79b..877611a8 100644 --- a/src/main/java/com/alibaba/dashscope/aigc/codegeneration/CodeGeneration.java +++ b/src/main/java/com/alibaba/dashscope/aigc/codegeneration/CodeGeneration.java @@ -1,16 +1,26 @@ +// Copyright (c) Alibaba, Inc. and its affiliates. package com.alibaba.dashscope.aigc.codegeneration; import com.alibaba.dashscope.api.SynchronizeHalfDuplexApi; -import com.alibaba.dashscope.common.*; +import com.alibaba.dashscope.common.DashScopeResult; +import com.alibaba.dashscope.common.Function; +import com.alibaba.dashscope.common.OutputMode; +import com.alibaba.dashscope.common.ResultCallback; +import com.alibaba.dashscope.common.Task; +import com.alibaba.dashscope.common.TaskGroup; import com.alibaba.dashscope.exception.ApiException; import com.alibaba.dashscope.exception.InputRequiredException; import com.alibaba.dashscope.exception.NoApiKeyException; -import com.alibaba.dashscope.protocol.*; +import com.alibaba.dashscope.protocol.ApiServiceOption; +import com.alibaba.dashscope.protocol.ConnectionOptions; +import com.alibaba.dashscope.protocol.HttpMethod; +import com.alibaba.dashscope.protocol.Protocol; +import com.alibaba.dashscope.protocol.StreamingMode; import io.reactivex.Flowable; import lombok.extern.slf4j.Slf4j; @Slf4j -public class CodeGeneration { +public final class CodeGeneration { private final SynchronizeHalfDuplexApi syncApi; private final ApiServiceOption serviceOption; diff --git a/src/main/java/com/alibaba/dashscope/aigc/completion/ChatCompletion.java b/src/main/java/com/alibaba/dashscope/aigc/completion/ChatCompletion.java index f6168da4..3a2a6d2d 100644 --- a/src/main/java/com/alibaba/dashscope/aigc/completion/ChatCompletion.java +++ b/src/main/java/com/alibaba/dashscope/aigc/completion/ChatCompletion.java @@ -1,3 +1,4 @@ +// Copyright (c) Alibaba, Inc. and its affiliates. package com.alibaba.dashscope.aigc.completion; import com.alibaba.dashscope.common.DashScopeResult; diff --git a/src/main/java/com/alibaba/dashscope/aigc/completion/ChatCompletionStreamOptions.java b/src/main/java/com/alibaba/dashscope/aigc/completion/ChatCompletionStreamOptions.java index 774f6f3c..101a8877 100644 --- a/src/main/java/com/alibaba/dashscope/aigc/completion/ChatCompletionStreamOptions.java +++ b/src/main/java/com/alibaba/dashscope/aigc/completion/ChatCompletionStreamOptions.java @@ -1,12 +1,16 @@ +// Copyright (c) Alibaba, Inc. and its affiliates. package com.alibaba.dashscope.aigc.completion; -import com.google.gson.annotations.SerializedName; import lombok.Data; +import lombok.EqualsAndHashCode; import lombok.experimental.SuperBuilder; +/** + * @deprecated Use {@link com.alibaba.dashscope.common.StreamOptions} instead for a shared + * implementation across all service classes. + */ +@Deprecated @Data @SuperBuilder -public class ChatCompletionStreamOptions { - @SerializedName("include_usage") - private Boolean includeUsage; -} +@EqualsAndHashCode(callSuper = true) +public class ChatCompletionStreamOptions extends com.alibaba.dashscope.common.StreamOptions {} diff --git a/src/main/java/com/alibaba/dashscope/aigc/completion/ChatCompletions.java b/src/main/java/com/alibaba/dashscope/aigc/completion/ChatCompletions.java index d0013df5..30aabfa9 100644 --- a/src/main/java/com/alibaba/dashscope/aigc/completion/ChatCompletions.java +++ b/src/main/java/com/alibaba/dashscope/aigc/completion/ChatCompletions.java @@ -1,3 +1,4 @@ +// Copyright (c) Alibaba, Inc. and its affiliates. package com.alibaba.dashscope.aigc.completion; import com.alibaba.dashscope.api.GeneralApi; diff --git a/src/main/java/com/alibaba/dashscope/aigc/generation/Generation.java b/src/main/java/com/alibaba/dashscope/aigc/generation/Generation.java index c329b66a..271733c2 100644 --- a/src/main/java/com/alibaba/dashscope/aigc/generation/Generation.java +++ b/src/main/java/com/alibaba/dashscope/aigc/generation/Generation.java @@ -18,8 +18,8 @@ import com.alibaba.dashscope.protocol.Protocol; import com.alibaba.dashscope.protocol.StreamingMode; import com.alibaba.dashscope.tools.ToolCallBase; -import com.alibaba.dashscope.tools.ToolCallFunction; import com.alibaba.dashscope.utils.ParamUtils; +import com.alibaba.dashscope.utils.StreamingMerger; import com.alibaba.dashscope.utils.StringUtils; import io.reactivex.Flowable; import java.util.ArrayList; @@ -64,6 +64,21 @@ private ApiServiceOption defaultApiServiceOption() { .build(); } + /** Creates a copy of the shared serviceOption for thread-safe per-call usage. */ + private ApiServiceOption copyServiceOption() { + return ApiServiceOption.builder() + .protocol(serviceOption.getProtocol()) + .httpMethod(serviceOption.getHttpMethod()) + .streamingMode(serviceOption.getStreamingMode()) + .outputMode(serviceOption.getOutputMode()) + .taskGroup(serviceOption.getTaskGroup()) + .task(serviceOption.getTask()) + .function(serviceOption.getFunction()) + .baseHttpUrl(serviceOption.getBaseHttpUrl()) + .baseWebSocketUrl(serviceOption.getBaseWebSocketUrl()) + .build(); + } + public Generation() { serviceOption = defaultApiServiceOption(); syncApi = new SynchronizeHalfDuplexApi<>(serviceOption); @@ -78,7 +93,7 @@ public Generation(String protocol) { public Generation(String protocol, String baseUrl) { serviceOption = defaultApiServiceOption(); serviceOption.setProtocol(Protocol.of(protocol)); - if (protocol.equals(Protocol.HTTP.getValue())) { + if (Protocol.HTTP.getValue().equals(protocol)) { serviceOption.setBaseHttpUrl(baseUrl); } else { serviceOption.setBaseWebSocketUrl(baseUrl); @@ -89,7 +104,7 @@ public Generation(String protocol, String baseUrl) { public Generation(String protocol, String baseUrl, ConnectionOptions connectionOptions) { serviceOption = defaultApiServiceOption(); serviceOption.setProtocol(Protocol.of(protocol)); - if (protocol.equals(Protocol.HTTP.getValue())) { + if (Protocol.HTTP.getValue().equals(protocol)) { serviceOption.setBaseHttpUrl(baseUrl); } else { serviceOption.setBaseWebSocketUrl(baseUrl); @@ -108,9 +123,10 @@ public Generation(String protocol, String baseUrl, ConnectionOptions connectionO public GenerationResult call(HalfDuplexServiceParam param) throws ApiException, NoApiKeyException, InputRequiredException { param.validate(); - serviceOption.setIsSSE(false); - serviceOption.setStreamingMode(StreamingMode.NONE); - return GenerationResult.fromDashScopeResult(syncApi.call(param)); + ApiServiceOption callOption = copyServiceOption(); + callOption.setIsSSE(false); + callOption.setStreamingMode(StreamingMode.NONE); + return GenerationResult.fromDashScopeResult(syncApi.call(param, callOption)); } /** @@ -125,8 +141,9 @@ public GenerationResult call(HalfDuplexServiceParam param) public void call(HalfDuplexServiceParam param, ResultCallback callback) throws ApiException, NoApiKeyException, InputRequiredException { param.validate(); - serviceOption.setIsSSE(false); - serviceOption.setStreamingMode(StreamingMode.NONE); + ApiServiceOption callOption = copyServiceOption(); + callOption.setIsSSE(false); + callOption.setStreamingMode(StreamingMode.NONE); syncApi.call( param, new ResultCallback() { @@ -144,7 +161,8 @@ public void onComplete() { public void onError(Exception e) { callback.onError(e); } - }); + }, + callOption); } /** @@ -168,10 +186,11 @@ public Flowable streamCall(HalfDuplexServiceParam param) String userAgentSuffix = StringUtils.format("incremental_to_full/%d", flagValue); param.putHeader("user-agent", userAgentSuffix); - serviceOption.setIsSSE(true); - serviceOption.setStreamingMode(StreamingMode.OUT); + ApiServiceOption callOption = copyServiceOption(); + callOption.setIsSSE(true); + callOption.setStreamingMode(StreamingMode.OUT); return syncApi - .streamCall(param) + .streamCall(param, callOption) .map(GenerationResult::fromDashScopeResult) .flatMap( result -> { @@ -181,16 +200,10 @@ public Flowable streamCall(HalfDuplexServiceParam param) } return Flowable.just(merged); }) - .doOnComplete( + .doFinally( () -> { if (toMergeResponse) { - clearAccumulatedData(); - } - }) - .doOnError( - throwable -> { - if (toMergeResponse) { - clearAccumulatedData(); + StreamingMerger.clearAccumulatedData(accumulatedDataMap); } }); } @@ -207,8 +220,9 @@ public void streamCall(HalfDuplexServiceParam param, ResultCallback() { @@ -224,7 +238,7 @@ public void onEvent(DashScopeResult msg) { @Override public void onComplete() { if (toMergeResponse) { - clearAccumulatedData(); + StreamingMerger.clearAccumulatedData(accumulatedDataMap); } callback.onComplete(); } @@ -232,11 +246,12 @@ public void onComplete() { @Override public void onError(Exception e) { if (toMergeResponse) { - clearAccumulatedData(); + StreamingMerger.clearAccumulatedData(accumulatedDataMap); } callback.onError(e); } - }); + }, + callOption); } /** @@ -344,10 +359,10 @@ private GenerationResult mergeSingleResponse( choice.getMessage().setReasoningContent(accumulated.reasoningContent.toString()); } - // Handle tool_calls accumulation + // Handle tool_calls accumulation (delegate to shared utility) List currentToolCalls = choice.getMessage().getToolCalls(); if (currentToolCalls != null && !currentToolCalls.isEmpty()) { - mergeToolCalls(currentToolCalls, accumulated.toolCalls); + StreamingMerger.mergeToolCalls(currentToolCalls, accumulated.toolCalls); } // Always set accumulated tool_calls if we have any if (!accumulated.toolCalls.isEmpty()) { @@ -526,110 +541,6 @@ private GenerationResult mergeSingleResponse( return result; } - /** Merges tool calls from current response with accumulated tool calls. */ - private void mergeToolCalls( - List currentToolCalls, List accumulatedToolCalls) { - for (ToolCallBase currentCall : currentToolCalls) { - if (currentCall == null || currentCall.getIndex() == null) { - continue; - } - - int index = currentCall.getIndex(); - - // Find existing accumulated call with same index - ToolCallBase existingCall = null; - for (ToolCallBase accCall : accumulatedToolCalls) { - if (accCall != null && accCall.getIndex() != null && accCall.getIndex().equals(index)) { - existingCall = accCall; - break; - } - } - - if (existingCall instanceof ToolCallFunction && currentCall instanceof ToolCallFunction) { - // Merge function calls - ToolCallFunction existingFunctionCall = (ToolCallFunction) existingCall; - ToolCallFunction currentFunctionCall = (ToolCallFunction) currentCall; - - if (currentFunctionCall.getFunction() != null) { - // Ensure existing function call has a function object - if (existingFunctionCall.getFunction() == null) { - existingFunctionCall.setFunction(existingFunctionCall.new CallFunction()); - } - - // Accumulate arguments if present - if (currentFunctionCall.getFunction().getArguments() != null) { - String existingArguments = existingFunctionCall.getFunction().getArguments(); - if (existingArguments == null) { - existingArguments = ""; - } - String currentArguments = currentFunctionCall.getFunction().getArguments(); - existingFunctionCall.getFunction().setArguments(existingArguments + currentArguments); - } - - // Accumulate function name if present - if (currentFunctionCall.getFunction().getName() != null) { - String existingName = existingFunctionCall.getFunction().getName(); - if (existingName == null) { - existingName = ""; - } - String currentName = currentFunctionCall.getFunction().getName(); - existingFunctionCall.getFunction().setName(existingName + currentName); - } - - // Update function output if present - if (currentFunctionCall.getFunction().getOutput() != null) { - existingFunctionCall - .getFunction() - .setOutput(currentFunctionCall.getFunction().getOutput()); - } - } - - // Update other fields with latest non-empty values - if (currentFunctionCall.getIndex() != null) { - existingFunctionCall.setIndex(currentFunctionCall.getIndex()); - } - if (currentFunctionCall.getId() != null && !currentFunctionCall.getId().isEmpty()) { - existingFunctionCall.setId(currentFunctionCall.getId()); - } - if (currentFunctionCall.getType() != null) { - existingFunctionCall.setType(currentFunctionCall.getType()); - } - } else { - // Add new tool call (create a copy) - if (currentCall instanceof ToolCallFunction) { - ToolCallFunction currentFunctionCall = (ToolCallFunction) currentCall; - ToolCallFunction newFunctionCall = new ToolCallFunction(); - newFunctionCall.setIndex(currentFunctionCall.getIndex()); - newFunctionCall.setId(currentFunctionCall.getId()); - newFunctionCall.setType(currentFunctionCall.getType()); - - if (currentFunctionCall.getFunction() != null) { - ToolCallFunction.CallFunction newCallFunction = newFunctionCall.new CallFunction(); - newCallFunction.setName(currentFunctionCall.getFunction().getName()); - newCallFunction.setArguments(currentFunctionCall.getFunction().getArguments()); - newCallFunction.setOutput(currentFunctionCall.getFunction().getOutput()); - newFunctionCall.setFunction(newCallFunction); - } - - accumulatedToolCalls.add(newFunctionCall); - } else { - // For other types of tool calls, add directly (assuming they are immutable or don't need - // merging) - accumulatedToolCalls.add(currentCall); - } - } - } - } - - /** - * Clears accumulated data for the current thread. Should be called when streaming is complete or - * encounters error. - */ - private void clearAccumulatedData() { - accumulatedDataMap.get().clear(); - accumulatedDataMap.remove(); - } - /** Inner class to store accumulated data for response merging. */ private static class AccumulatedData { StringBuilder content = new StringBuilder(); diff --git a/src/main/java/com/alibaba/dashscope/aigc/generation/GenerationOutput.java b/src/main/java/com/alibaba/dashscope/aigc/generation/GenerationOutput.java index d8eae904..4f4f2b6c 100644 --- a/src/main/java/com/alibaba/dashscope/aigc/generation/GenerationOutput.java +++ b/src/main/java/com/alibaba/dashscope/aigc/generation/GenerationOutput.java @@ -20,6 +20,10 @@ public class Choice { private Message message; private GenerationLogprobs logprobs; + + /** Search information for this specific choice, returned when search_options is configured. */ + @SerializedName("search_info") + private SearchInfo searchInfo; } // output text private String text; diff --git a/src/main/java/com/alibaba/dashscope/aigc/generation/GenerationParam.java b/src/main/java/com/alibaba/dashscope/aigc/generation/GenerationParam.java index 17269c5a..877443b9 100644 --- a/src/main/java/com/alibaba/dashscope/aigc/generation/GenerationParam.java +++ b/src/main/java/com/alibaba/dashscope/aigc/generation/GenerationParam.java @@ -13,6 +13,7 @@ import com.alibaba.dashscope.common.ResponseFormat; import com.alibaba.dashscope.common.Role; import com.alibaba.dashscope.common.SearchOptions; +import com.alibaba.dashscope.common.StreamOptions; import com.alibaba.dashscope.exception.InputRequiredException; import com.alibaba.dashscope.tools.ToolBase; import com.alibaba.dashscope.utils.ApiKeywords; @@ -36,8 +37,8 @@ @SuperBuilder public class GenerationParam extends GenerationParamBase { public static class ResultFormat { - public static String TEXT = "text"; - public static String MESSAGE = "message"; + public static final String TEXT = "text"; + public static final String MESSAGE = "message"; } private List messages; @@ -103,6 +104,18 @@ public static class ResultFormat { /** repetition penalty */ private Float repetitionPenalty; + /** + * Presence penalty: reduces the model's likelihood to repeat the exact tokens that have already + * appeared in the output. A value of 0 indicates no penalty. Default value: 0.0 + */ + private Float presencePenalty; + + /** + * Frequency penalty: reduces the model's likelihood to repeat tokens based on their frequency in + * the output so far. A value of 0 indicates no penalty. Default value: 0.0 + */ + private Float frequencyPenalty; + /** stopString and token are mutually exclusive. */ @Singular("stopString") private List stopStrings; @@ -143,6 +156,9 @@ public static class ResultFormat { /** 翻译参数 */ private TranslationOptions translationOptions; + /** Streaming options controlling what extra info is included in stream chunks. */ + private StreamOptions streamOptions; + @Override public JsonObject getInput() { JsonObject jsonObject = new JsonObject(); @@ -207,6 +223,14 @@ public Map getParameters() { if (repetitionPenalty != null) { params.put(REPETITION_PENALTY, repetitionPenalty); } + + if (presencePenalty != null) { + params.put("presence_penalty", presencePenalty); + } + + if (frequencyPenalty != null) { + params.put("frequency_penalty", frequencyPenalty); + } if (maxTokens != null) { params.put(MAX_TOKENS, maxTokens); } @@ -262,6 +286,10 @@ public Map getParameters() { params.put("translation_options", translationOptions); } + if (streamOptions != null) { + params.put("stream_options", streamOptions); + } + params.putAll(parameters); return params; } diff --git a/src/main/java/com/alibaba/dashscope/aigc/generation/GenerationUsage.java b/src/main/java/com/alibaba/dashscope/aigc/generation/GenerationUsage.java index 4a338b60..608793bb 100644 --- a/src/main/java/com/alibaba/dashscope/aigc/generation/GenerationUsage.java +++ b/src/main/java/com/alibaba/dashscope/aigc/generation/GenerationUsage.java @@ -1,6 +1,7 @@ // Copyright (c) Alibaba, Inc. and its affiliates. package com.alibaba.dashscope.aigc.generation; +import com.alibaba.dashscope.common.Plugins; import com.google.gson.annotations.SerializedName; import lombok.Data; import lombok.experimental.SuperBuilder; @@ -35,23 +36,6 @@ public static class CacheCreation { private CacheCreation cacheCreation; } - @Data - @SuperBuilder - public static class Plugins { - - @Data - public static class Search { - @SerializedName("count") - private Integer count; - - @SerializedName("strategy") - private String strategy; - } - - @SerializedName("search") - private Search search; - } - @SerializedName("input_tokens") private Integer inputTokens; diff --git a/src/main/java/com/alibaba/dashscope/aigc/imagegeneration/ImageGeneration.java b/src/main/java/com/alibaba/dashscope/aigc/imagegeneration/ImageGeneration.java index 4864ed8b..7fc0be78 100644 --- a/src/main/java/com/alibaba/dashscope/aigc/imagegeneration/ImageGeneration.java +++ b/src/main/java/com/alibaba/dashscope/aigc/imagegeneration/ImageGeneration.java @@ -3,18 +3,27 @@ import com.alibaba.dashscope.api.AsynchronousApi; import com.alibaba.dashscope.api.SynchronizeHalfDuplexApi; -import com.alibaba.dashscope.common.*; +import com.alibaba.dashscope.common.DashScopeResult; +import com.alibaba.dashscope.common.Function; +import com.alibaba.dashscope.common.OutputMode; +import com.alibaba.dashscope.common.ResultCallback; +import com.alibaba.dashscope.common.Role; +import com.alibaba.dashscope.common.Task; +import com.alibaba.dashscope.common.TaskGroup; import com.alibaba.dashscope.exception.ApiException; import com.alibaba.dashscope.exception.InputRequiredException; import com.alibaba.dashscope.exception.NoApiKeyException; import com.alibaba.dashscope.exception.UploadFileException; -import com.alibaba.dashscope.protocol.*; +import com.alibaba.dashscope.protocol.ApiServiceOption; +import com.alibaba.dashscope.protocol.ConnectionOptions; +import com.alibaba.dashscope.protocol.HttpMethod; +import com.alibaba.dashscope.protocol.Protocol; +import com.alibaba.dashscope.protocol.StreamingMode; import com.alibaba.dashscope.task.AsyncTaskListParam; -import com.alibaba.dashscope.tools.ToolCallBase; -import com.alibaba.dashscope.tools.ToolCallFunction; import com.alibaba.dashscope.utils.OSSUploadCertificate; import com.alibaba.dashscope.utils.ParamUtils; import com.alibaba.dashscope.utils.PreprocessMessageInput; +import com.alibaba.dashscope.utils.StreamingMerger; import com.alibaba.dashscope.utils.StringUtils; import io.reactivex.Flowable; import java.util.ArrayList; @@ -50,6 +59,21 @@ private ApiServiceOption defaultSyncApiServiceOption() { .build(); } + /** Creates a copy of the shared serviceOption for thread-safe per-call usage. */ + private ApiServiceOption copyServiceOption() { + return ApiServiceOption.builder() + .protocol(serviceOption.getProtocol()) + .httpMethod(serviceOption.getHttpMethod()) + .streamingMode(serviceOption.getStreamingMode()) + .outputMode(serviceOption.getOutputMode()) + .taskGroup(serviceOption.getTaskGroup()) + .task(serviceOption.getTask()) + .function(serviceOption.getFunction()) + .baseHttpUrl(serviceOption.getBaseHttpUrl()) + .baseWebSocketUrl(serviceOption.getBaseWebSocketUrl()) + .build(); + } + public ImageGeneration() { serviceOption = defaultSyncApiServiceOption(); syncApi = new SynchronizeHalfDuplexApi<>(serviceOption); @@ -103,11 +127,12 @@ public ImageGeneration(String protocol, String baseUrl, ConnectionOptions connec */ public ImageGenerationResult call(ImageGenerationParam param) throws ApiException, NoApiKeyException, UploadFileException { - serviceOption.setIsSSE(false); - serviceOption.setStreamingMode(StreamingMode.NONE); - serviceOption.setTask(Task.MULTIMODAL_GENERATION.getValue()); + ApiServiceOption callOption = copyServiceOption(); + callOption.setIsSSE(false); + callOption.setStreamingMode(StreamingMode.NONE); + callOption.setTask(Task.MULTIMODAL_GENERATION.getValue()); preprocessInput(param); - return ImageGenerationResult.fromDashScopeResult(syncApi.call(param)); + return ImageGenerationResult.fromDashScopeResult(syncApi.call(param, callOption)); } /** @@ -122,9 +147,10 @@ public ImageGenerationResult call(ImageGenerationParam param) */ public void call(ImageGenerationParam param, ResultCallback callback) throws ApiException, NoApiKeyException, UploadFileException { - serviceOption.setIsSSE(false); - serviceOption.setStreamingMode(StreamingMode.NONE); - serviceOption.setTask(Task.MULTIMODAL_GENERATION.getValue()); + ApiServiceOption callOption = copyServiceOption(); + callOption.setIsSSE(false); + callOption.setStreamingMode(StreamingMode.NONE); + callOption.setTask(Task.MULTIMODAL_GENERATION.getValue()); preprocessInput(param); syncApi.call( param, @@ -143,7 +169,8 @@ public void onComplete() { public void onError(Exception e) { callback.onError(e); } - }); + }, + callOption); } /** @@ -157,9 +184,10 @@ public void onError(Exception e) { public ImageGenerationResult asyncCall(ImageGenerationParam param) throws ApiException, NoApiKeyException, UploadFileException { preprocessInput(param); - serviceOption.setTask(Task.IMAGE_GENERATION.getValue()); - serviceOption.setIsAsyncTask(true); - return ImageGenerationResult.fromDashScopeResult(asyncApi.asyncCall(param, serviceOption)); + ApiServiceOption callOption = copyServiceOption(); + callOption.setTask(Task.IMAGE_GENERATION.getValue()); + callOption.setIsAsyncTask(true); + return ImageGenerationResult.fromDashScopeResult(asyncApi.asyncCall(param, callOption)); } public ImageGenerationListResult list(AsyncTaskListParam param) @@ -234,24 +262,19 @@ public Flowable streamCall(ImageGenerationParam param) String userAgentSuffix = StringUtils.format("incremental_to_full/%d", flagValue); param.putHeader("user-agent", userAgentSuffix); - serviceOption.setIsSSE(true); - serviceOption.setStreamingMode(StreamingMode.OUT); - serviceOption.setTask(Task.MULTIMODAL_GENERATION.getValue()); + ApiServiceOption callOption = copyServiceOption(); + callOption.setIsSSE(true); + callOption.setStreamingMode(StreamingMode.OUT); + callOption.setTask(Task.MULTIMODAL_GENERATION.getValue()); preprocessInput(param); return syncApi - .streamCall(param) + .streamCall(param, callOption) .map(ImageGenerationResult::fromDashScopeResult) .map(result -> mergeSingleResponse(result, toMergeResponse)) - .doOnComplete( + .doFinally( () -> { if (toMergeResponse) { - clearAccumulatedData(); - } - }) - .doOnError( - throwable -> { - if (toMergeResponse) { - clearAccumulatedData(); + StreamingMerger.clearAccumulatedData(accumulatedDataMap); } }); } @@ -278,9 +301,10 @@ public void streamCall(ImageGenerationParam param, ResultCallback> currentContent = choice.getMessage().getContent(); if (currentContent != null && !currentContent.isEmpty()) { - mergeTextContent(currentContent, accumulated); + StreamingMerger.mergeTextContent(currentContent, accumulated.content); } // Always set the accumulated content if we have any if (!accumulated.content.isEmpty()) { @@ -395,147 +422,6 @@ private ImageGenerationResult mergeSingleResponse( return result; } - /** - * Merges text content from current response with accumulated content. For MultiModal, content is - * a List> where text content is in maps with "text" key. - */ - private void mergeTextContent( - List> currentContent, AccumulatedData accumulated) { - for (Map contentItem : currentContent) { - if (contentItem.containsKey("text")) { - String textValue = (String) contentItem.get("text"); - if (textValue != null && !textValue.isEmpty()) { - // Find or create text content item in accumulated content - Map accumulatedTextItem = null; - for (Map accItem : accumulated.content) { - if (accItem.containsKey("text")) { - accumulatedTextItem = accItem; - break; - } - } - - if (accumulatedTextItem == null) { - // Create new text content item - accumulatedTextItem = new HashMap<>(); - accumulatedTextItem.put("text", textValue); - accumulated.content.add(accumulatedTextItem); - } else { - // Append to existing text content - String existingText = (String) accumulatedTextItem.get("text"); - if (existingText == null) { - existingText = ""; - } - accumulatedTextItem.put("text", existingText + textValue); - } - } - } - } - } - - /** Merges tool calls from current response with accumulated tool calls. */ - private void mergeToolCalls( - List currentToolCalls, List accumulatedToolCalls) { - for (ToolCallBase currentCall : currentToolCalls) { - if (currentCall == null || currentCall.getIndex() == null) { - continue; - } - - int index = currentCall.getIndex(); - - // Find existing accumulated call with same index - ToolCallBase existingCall = null; - for (ToolCallBase accCall : accumulatedToolCalls) { - if (accCall != null && accCall.getIndex() != null && accCall.getIndex().equals(index)) { - existingCall = accCall; - break; - } - } - - if (existingCall instanceof ToolCallFunction && currentCall instanceof ToolCallFunction) { - // Merge function calls - ToolCallFunction existingFunctionCall = (ToolCallFunction) existingCall; - ToolCallFunction currentFunctionCall = (ToolCallFunction) currentCall; - - if (currentFunctionCall.getFunction() != null) { - // Ensure existing function call has a function object - if (existingFunctionCall.getFunction() == null) { - existingFunctionCall.setFunction(existingFunctionCall.new CallFunction()); - } - - // Accumulate arguments if present - if (currentFunctionCall.getFunction().getArguments() != null) { - String existingArguments = existingFunctionCall.getFunction().getArguments(); - if (existingArguments == null) { - existingArguments = ""; - } - String currentArguments = currentFunctionCall.getFunction().getArguments(); - existingFunctionCall.getFunction().setArguments(existingArguments + currentArguments); - } - - // Accumulate function name if present - if (currentFunctionCall.getFunction().getName() != null) { - String existingName = existingFunctionCall.getFunction().getName(); - if (existingName == null) { - existingName = ""; - } - String currentName = currentFunctionCall.getFunction().getName(); - existingFunctionCall.getFunction().setName(existingName + currentName); - } - - // Update function output if present - if (currentFunctionCall.getFunction().getOutput() != null) { - existingFunctionCall - .getFunction() - .setOutput(currentFunctionCall.getFunction().getOutput()); - } - } - - // Update other fields with latest non-empty values - if (currentFunctionCall.getIndex() != null) { - existingFunctionCall.setIndex(currentFunctionCall.getIndex()); - } - if (currentFunctionCall.getId() != null && !currentFunctionCall.getId().isEmpty()) { - existingFunctionCall.setId(currentFunctionCall.getId()); - } - if (currentFunctionCall.getType() != null) { - existingFunctionCall.setType(currentFunctionCall.getType()); - } - } else { - // Add new tool call (create a copy) - if (currentCall instanceof ToolCallFunction) { - ToolCallFunction currentFunctionCall = (ToolCallFunction) currentCall; - ToolCallFunction newFunctionCall = new ToolCallFunction(); - newFunctionCall.setIndex(currentFunctionCall.getIndex()); - newFunctionCall.setId(currentFunctionCall.getId()); - newFunctionCall.setType(currentFunctionCall.getType()); - - if (currentFunctionCall.getFunction() != null) { - ToolCallFunction.CallFunction newCallFunction = newFunctionCall.new CallFunction(); - newCallFunction.setName(currentFunctionCall.getFunction().getName()); - newCallFunction.setArguments(currentFunctionCall.getFunction().getArguments()); - newCallFunction.setOutput(currentFunctionCall.getFunction().getOutput()); - newFunctionCall.setFunction(newCallFunction); - } - - accumulatedToolCalls.add(newFunctionCall); - } else { - // For other types of tool calls, add directly (assuming they are immutable or don't need - // merging) - accumulatedToolCalls.add(currentCall); - } - } - } - } - - /** - * Clears accumulated data for the current thread. Should be called when streaming is complete or - * encounters error. - */ - private void clearAccumulatedData() { - accumulatedDataMap.get().clear(); - accumulatedDataMap.remove(); - } - /** Inner class to store accumulated data for response merging. */ private static class AccumulatedData { List> content = new ArrayList<>(); diff --git a/src/main/java/com/alibaba/dashscope/aigc/imagesynthesis/ImageSynthesis.java b/src/main/java/com/alibaba/dashscope/aigc/imagesynthesis/ImageSynthesis.java index 68256bb7..8e4825b3 100644 --- a/src/main/java/com/alibaba/dashscope/aigc/imagesynthesis/ImageSynthesis.java +++ b/src/main/java/com/alibaba/dashscope/aigc/imagesynthesis/ImageSynthesis.java @@ -6,6 +6,7 @@ import com.alibaba.dashscope.base.HalfDuplexServiceParam; import com.alibaba.dashscope.common.DashScopeResult; import com.alibaba.dashscope.exception.ApiException; +import com.alibaba.dashscope.exception.InputRequiredException; import com.alibaba.dashscope.exception.NoApiKeyException; import com.alibaba.dashscope.exception.UploadFileException; import com.alibaba.dashscope.protocol.ApiServiceOption; @@ -111,12 +112,12 @@ public ImageSynthesis(String task, String baseUrl) { } public ImageSynthesisResult asyncCall(ImageSynthesisParam param) - throws ApiException, NoApiKeyException { + throws ApiException, NoApiKeyException, InputRequiredException { // add local file support try { param.checkAndUpload(); } catch (UploadFileException e) { - throw new ApiException(e); + throw new InputRequiredException(e.getMessage()); } ApiServiceOption serviceOption = createServiceOptions; if (param.getModel().contains("imageedit") || param.getModel().contains("wan2.5-i2i")) { @@ -130,12 +131,12 @@ public ImageSynthesisResult asyncCall(ImageSynthesisParam param) * models will result in an error,More raw image models may be added for use later */ public ImageSynthesisResult syncCall(ImageSynthesisParam param) - throws ApiException, NoApiKeyException { + throws ApiException, NoApiKeyException, InputRequiredException { // add local file support try { param.checkAndUpload(); } catch (UploadFileException e) { - throw new ApiException(e); + throw new InputRequiredException(e.getMessage()); } ApiServiceOption serviceOption = createServiceOptions; serviceOption.setIsAsyncTask(false); @@ -151,12 +152,12 @@ public ImageSynthesisResult syncCall(ImageSynthesisParam param) * @throws ApiException The request failed, possibly due to a network or data error. */ public ImageSynthesisResult call(ImageSynthesisParam param) - throws ApiException, NoApiKeyException { + throws ApiException, NoApiKeyException, InputRequiredException { // add local file support try { param.checkAndUpload(); } catch (UploadFileException e) { - throw new ApiException(e); + throw new InputRequiredException(e.getMessage()); } ApiServiceOption serviceOption = createServiceOptions; if (param.getModel().contains("imageedit") || param.getModel().contains("wan2.5-i2i")) { diff --git a/src/main/java/com/alibaba/dashscope/aigc/multimodalconversation/MultiModalConversation.java b/src/main/java/com/alibaba/dashscope/aigc/multimodalconversation/MultiModalConversation.java index 93b80594..9bafe611 100644 --- a/src/main/java/com/alibaba/dashscope/aigc/multimodalconversation/MultiModalConversation.java +++ b/src/main/java/com/alibaba/dashscope/aigc/multimodalconversation/MultiModalConversation.java @@ -2,17 +2,28 @@ package com.alibaba.dashscope.aigc.multimodalconversation; import com.alibaba.dashscope.api.SynchronizeHalfDuplexApi; -import com.alibaba.dashscope.common.*; +import com.alibaba.dashscope.common.DashScopeResult; +import com.alibaba.dashscope.common.Function; +import com.alibaba.dashscope.common.MultiModalMessage; +import com.alibaba.dashscope.common.OutputMode; +import com.alibaba.dashscope.common.ResultCallback; +import com.alibaba.dashscope.common.Role; +import com.alibaba.dashscope.common.Task; +import com.alibaba.dashscope.common.TaskGroup; import com.alibaba.dashscope.exception.ApiException; import com.alibaba.dashscope.exception.InputRequiredException; import com.alibaba.dashscope.exception.NoApiKeyException; import com.alibaba.dashscope.exception.UploadFileException; -import com.alibaba.dashscope.protocol.*; +import com.alibaba.dashscope.protocol.ApiServiceOption; +import com.alibaba.dashscope.protocol.ConnectionOptions; +import com.alibaba.dashscope.protocol.HttpMethod; +import com.alibaba.dashscope.protocol.Protocol; +import com.alibaba.dashscope.protocol.StreamingMode; import com.alibaba.dashscope.tools.ToolCallBase; -import com.alibaba.dashscope.tools.ToolCallFunction; import com.alibaba.dashscope.utils.OSSUploadCertificate; import com.alibaba.dashscope.utils.ParamUtils; import com.alibaba.dashscope.utils.PreprocessMessageInput; +import com.alibaba.dashscope.utils.StreamingMerger; import com.alibaba.dashscope.utils.StringUtils; import io.reactivex.Flowable; import java.util.ArrayList; @@ -47,6 +58,21 @@ private ApiServiceOption defaultApiServiceOption() { .build(); } + /** Creates a copy of the shared serviceOption for thread-safe per-call usage. */ + private ApiServiceOption copyServiceOption() { + return ApiServiceOption.builder() + .protocol(serviceOption.getProtocol()) + .httpMethod(serviceOption.getHttpMethod()) + .streamingMode(serviceOption.getStreamingMode()) + .outputMode(serviceOption.getOutputMode()) + .taskGroup(serviceOption.getTaskGroup()) + .task(serviceOption.getTask()) + .function(serviceOption.getFunction()) + .baseHttpUrl(serviceOption.getBaseHttpUrl()) + .baseWebSocketUrl(serviceOption.getBaseWebSocketUrl()) + .build(); + } + public MultiModalConversation() { serviceOption = defaultApiServiceOption(); syncApi = new SynchronizeHalfDuplexApi<>(serviceOption); @@ -90,10 +116,11 @@ public MultiModalConversation( */ public MultiModalConversationResult call(MultiModalConversationParam param) throws ApiException, NoApiKeyException, UploadFileException { - serviceOption.setIsSSE(false); - serviceOption.setStreamingMode(StreamingMode.NONE); + ApiServiceOption callOption = copyServiceOption(); + callOption.setIsSSE(false); + callOption.setStreamingMode(StreamingMode.NONE); preprocessInput(param); - return MultiModalConversationResult.fromDashScopeResult(syncApi.call(param)); + return MultiModalConversationResult.fromDashScopeResult(syncApi.call(param, callOption)); } /** @@ -109,8 +136,9 @@ public MultiModalConversationResult call(MultiModalConversationParam param) public void call( MultiModalConversationParam param, ResultCallback callback) throws ApiException, NoApiKeyException, UploadFileException { - serviceOption.setIsSSE(false); - serviceOption.setStreamingMode(StreamingMode.NONE); + ApiServiceOption callOption = copyServiceOption(); + callOption.setIsSSE(false); + callOption.setStreamingMode(StreamingMode.NONE); preprocessInput(param); syncApi.call( param, @@ -129,7 +157,8 @@ public void onComplete() { public void onError(Exception e) { callback.onError(e); } - }); + }, + callOption); } /** @@ -151,23 +180,18 @@ public Flowable streamCall(MultiModalConversationP String userAgentSuffix = StringUtils.format("incremental_to_full/%d", flagValue); param.putHeader("user-agent", userAgentSuffix); - serviceOption.setIsSSE(true); - serviceOption.setStreamingMode(StreamingMode.OUT); + ApiServiceOption callOption = copyServiceOption(); + callOption.setIsSSE(true); + callOption.setStreamingMode(StreamingMode.OUT); preprocessInput(param); return syncApi - .streamCall(param) + .streamCall(param, callOption) .map(MultiModalConversationResult::fromDashScopeResult) .map(result -> mergeSingleResponse(result, toMergeResponse)) - .doOnComplete( + .doFinally( () -> { if (toMergeResponse) { - clearAccumulatedData(); - } - }) - .doOnError( - throwable -> { - if (toMergeResponse) { - clearAccumulatedData(); + StreamingMerger.clearAccumulatedData(accumulatedDataMap); } }); } @@ -195,8 +219,9 @@ public void streamCall( String userAgentSuffix = StringUtils.format("incremental_to_full/%d", flagValue); param.putHeader("user-agent", userAgentSuffix); - serviceOption.setIsSSE(true); - serviceOption.setStreamingMode(StreamingMode.OUT); + ApiServiceOption callOption = copyServiceOption(); + callOption.setIsSSE(true); + callOption.setStreamingMode(StreamingMode.OUT); preprocessInput(param); syncApi.streamCall( param, @@ -207,13 +232,15 @@ public void onEvent(DashScopeResult msg) { MultiModalConversationResult.fromDashScopeResult(msg); MultiModalConversationResult mergedResult = mergeSingleResponse(result, toMergeResponse); - callback.onEvent(mergedResult); + if (mergedResult != null) { + callback.onEvent(mergedResult); + } } @Override public void onComplete() { if (toMergeResponse) { - clearAccumulatedData(); + StreamingMerger.clearAccumulatedData(accumulatedDataMap); } callback.onComplete(); } @@ -221,11 +248,12 @@ public void onComplete() { @Override public void onError(Exception e) { if (toMergeResponse) { - clearAccumulatedData(); + StreamingMerger.clearAccumulatedData(accumulatedDataMap); } callback.onError(e); } - }); + }, + callOption); } private void preprocessInput(MultiModalConversationParam param) @@ -315,7 +343,7 @@ private MultiModalConversationResult mergeSingleResponse( // Handle content accumulation (text content in content list) List> currentContent = choice.getMessage().getContent(); if (currentContent != null && !currentContent.isEmpty()) { - mergeTextContent(currentContent, accumulated); + StreamingMerger.mergeTextContent(currentContent, accumulated.content); } // Always set the accumulated content if we have any if (!accumulated.content.isEmpty()) { @@ -332,10 +360,10 @@ private MultiModalConversationResult mergeSingleResponse( choice.getMessage().setReasoningContent(accumulated.reasoningContent.toString()); } - // Handle tool_calls accumulation + // Handle tool_calls accumulation (delegate to shared utility) List currentToolCalls = choice.getMessage().getToolCalls(); if (currentToolCalls != null && !currentToolCalls.isEmpty()) { - mergeToolCalls(currentToolCalls, accumulated.toolCalls); + StreamingMerger.mergeToolCalls(currentToolCalls, accumulated.toolCalls); } // Always set accumulated tool_calls if we have any if (!accumulated.toolCalls.isEmpty()) { @@ -348,147 +376,6 @@ private MultiModalConversationResult mergeSingleResponse( return result; } - /** - * Merges text content from current response with accumulated content. For MultiModal, content is - * a List> where text content is in maps with "text" key. - */ - private void mergeTextContent( - List> currentContent, AccumulatedData accumulated) { - for (Map contentItem : currentContent) { - if (contentItem.containsKey("text")) { - String textValue = (String) contentItem.get("text"); - if (textValue != null && !textValue.isEmpty()) { - // Find or create text content item in accumulated content - Map accumulatedTextItem = null; - for (Map accItem : accumulated.content) { - if (accItem.containsKey("text")) { - accumulatedTextItem = accItem; - break; - } - } - - if (accumulatedTextItem == null) { - // Create new text content item - accumulatedTextItem = new HashMap<>(); - accumulatedTextItem.put("text", textValue); - accumulated.content.add(accumulatedTextItem); - } else { - // Append to existing text content - String existingText = (String) accumulatedTextItem.get("text"); - if (existingText == null) { - existingText = ""; - } - accumulatedTextItem.put("text", existingText + textValue); - } - } - } - } - } - - /** Merges tool calls from current response with accumulated tool calls. */ - private void mergeToolCalls( - List currentToolCalls, List accumulatedToolCalls) { - for (ToolCallBase currentCall : currentToolCalls) { - if (currentCall == null || currentCall.getIndex() == null) { - continue; - } - - int index = currentCall.getIndex(); - - // Find existing accumulated call with same index - ToolCallBase existingCall = null; - for (ToolCallBase accCall : accumulatedToolCalls) { - if (accCall != null && accCall.getIndex() != null && accCall.getIndex().equals(index)) { - existingCall = accCall; - break; - } - } - - if (existingCall instanceof ToolCallFunction && currentCall instanceof ToolCallFunction) { - // Merge function calls - ToolCallFunction existingFunctionCall = (ToolCallFunction) existingCall; - ToolCallFunction currentFunctionCall = (ToolCallFunction) currentCall; - - if (currentFunctionCall.getFunction() != null) { - // Ensure existing function call has a function object - if (existingFunctionCall.getFunction() == null) { - existingFunctionCall.setFunction(existingFunctionCall.new CallFunction()); - } - - // Accumulate arguments if present - if (currentFunctionCall.getFunction().getArguments() != null) { - String existingArguments = existingFunctionCall.getFunction().getArguments(); - if (existingArguments == null) { - existingArguments = ""; - } - String currentArguments = currentFunctionCall.getFunction().getArguments(); - existingFunctionCall.getFunction().setArguments(existingArguments + currentArguments); - } - - // Accumulate function name if present - if (currentFunctionCall.getFunction().getName() != null) { - String existingName = existingFunctionCall.getFunction().getName(); - if (existingName == null) { - existingName = ""; - } - String currentName = currentFunctionCall.getFunction().getName(); - existingFunctionCall.getFunction().setName(existingName + currentName); - } - - // Update function output if present - if (currentFunctionCall.getFunction().getOutput() != null) { - existingFunctionCall - .getFunction() - .setOutput(currentFunctionCall.getFunction().getOutput()); - } - } - - // Update other fields with latest non-empty values - if (currentFunctionCall.getIndex() != null) { - existingFunctionCall.setIndex(currentFunctionCall.getIndex()); - } - if (currentFunctionCall.getId() != null && !currentFunctionCall.getId().isEmpty()) { - existingFunctionCall.setId(currentFunctionCall.getId()); - } - if (currentFunctionCall.getType() != null) { - existingFunctionCall.setType(currentFunctionCall.getType()); - } - } else { - // Add new tool call (create a copy) - if (currentCall instanceof ToolCallFunction) { - ToolCallFunction currentFunctionCall = (ToolCallFunction) currentCall; - ToolCallFunction newFunctionCall = new ToolCallFunction(); - newFunctionCall.setIndex(currentFunctionCall.getIndex()); - newFunctionCall.setId(currentFunctionCall.getId()); - newFunctionCall.setType(currentFunctionCall.getType()); - - if (currentFunctionCall.getFunction() != null) { - ToolCallFunction.CallFunction newCallFunction = newFunctionCall.new CallFunction(); - newCallFunction.setName(currentFunctionCall.getFunction().getName()); - newCallFunction.setArguments(currentFunctionCall.getFunction().getArguments()); - newCallFunction.setOutput(currentFunctionCall.getFunction().getOutput()); - newFunctionCall.setFunction(newCallFunction); - } - - accumulatedToolCalls.add(newFunctionCall); - } else { - // For other types of tool calls, add directly (assuming they are immutable or don't need - // merging) - accumulatedToolCalls.add(currentCall); - } - } - } - } - - /** - * Clears accumulated data for the current thread. Should be called when streaming is complete or - * encounters error. - */ - private void clearAccumulatedData() { - accumulatedDataMap.get().clear(); - accumulatedDataMap.remove(); - } - /** Inner class to store accumulated data for response merging. */ private static class AccumulatedData { List> content = new ArrayList<>(); diff --git a/src/main/java/com/alibaba/dashscope/aigc/multimodalconversation/MultiModalConversationOutput.java b/src/main/java/com/alibaba/dashscope/aigc/multimodalconversation/MultiModalConversationOutput.java index f2a00c7f..c8c420cb 100644 --- a/src/main/java/com/alibaba/dashscope/aigc/multimodalconversation/MultiModalConversationOutput.java +++ b/src/main/java/com/alibaba/dashscope/aigc/multimodalconversation/MultiModalConversationOutput.java @@ -14,7 +14,13 @@ public static class Choice { @SerializedName("finish_reason") private String finishReason; + private Integer index; + private MultiModalMessage message; + + /** Search information for this specific choice, returned when search_options is configured. */ + @SerializedName("search_info") + private SearchInfo searchInfo; } private List choices; diff --git a/src/main/java/com/alibaba/dashscope/aigc/multimodalconversation/MultiModalConversationParam.java b/src/main/java/com/alibaba/dashscope/aigc/multimodalconversation/MultiModalConversationParam.java index 202bfa53..95bb5c55 100644 --- a/src/main/java/com/alibaba/dashscope/aigc/multimodalconversation/MultiModalConversationParam.java +++ b/src/main/java/com/alibaba/dashscope/aigc/multimodalconversation/MultiModalConversationParam.java @@ -3,6 +3,7 @@ import com.alibaba.dashscope.base.HalfDuplexServiceParam; import com.alibaba.dashscope.common.ResponseFormat; import com.alibaba.dashscope.common.SearchOptions; +import com.alibaba.dashscope.common.StreamOptions; import com.alibaba.dashscope.exception.InputRequiredException; import com.alibaba.dashscope.tools.ToolBase; import com.alibaba.dashscope.utils.ApiKeywords; @@ -55,6 +56,12 @@ public class MultiModalConversationParam extends HalfDuplexServiceParam { */ private Float presencePenalty; + /** + * Frequency penalty: reduces the model's likelihood to repeat tokens based on their frequency in + * the output so far. A value of 0 indicates no penalty. Default value: 0.0 + */ + private Float frequencyPenalty; + /* Whether to enable web search(quark). Currently works best only on the first round of conversation. Default to False */ @@ -103,7 +110,7 @@ public class MultiModalConversationParam extends HalfDuplexServiceParam { * apple * */ - @Builder.Default private Boolean incrementalOutput; + @Builder.Default private Boolean incrementalOutput = false; /** Output format of the model including "text" and "audio". Default value: ["text"] */ private List modalities; @@ -162,6 +169,9 @@ public class MultiModalConversationParam extends HalfDuplexServiceParam { /** thinking budget */ private Integer thinkingBudget; + /** Streaming options controlling what extra info is included in stream chunks. */ + private StreamOptions streamOptions; + @Override public JsonObject getHttpBody() { JsonObject requestObject = new JsonObject(); @@ -239,6 +249,10 @@ public Map getParameters() { params.put(ApiKeywords.PRESENCE_PENALTY, presencePenalty); } + if (frequencyPenalty != null) { + params.put("frequency_penalty", frequencyPenalty); + } + // Apply different logic based on model version if (ParamUtils.isQwenVersionThreeOrHigher(getModel())) { if (incrementalOutput != null) { @@ -318,6 +332,10 @@ public Map getParameters() { params.put("thinking_budget", thinkingBudget); } + if (streamOptions != null) { + params.put("stream_options", streamOptions); + } + params.putAll(parameters); return params; } diff --git a/src/main/java/com/alibaba/dashscope/aigc/multimodalconversation/MultiModalConversationUsage.java b/src/main/java/com/alibaba/dashscope/aigc/multimodalconversation/MultiModalConversationUsage.java index 2ec3934e..99df869b 100644 --- a/src/main/java/com/alibaba/dashscope/aigc/multimodalconversation/MultiModalConversationUsage.java +++ b/src/main/java/com/alibaba/dashscope/aigc/multimodalconversation/MultiModalConversationUsage.java @@ -1,27 +1,13 @@ +// Copyright (c) Alibaba, Inc. and its affiliates. package com.alibaba.dashscope.aigc.multimodalconversation; +import com.alibaba.dashscope.common.Plugins; import com.google.gson.annotations.SerializedName; import lombok.Data; @Data public class MultiModalConversationUsage { - @Data - public static class Plugins { - - @Data - public static class Search { - @SerializedName("count") - private Integer count; - - @SerializedName("strategy") - private String strategy; - } - - @SerializedName("search") - private Search search; - } - @SerializedName("input_tokens") private Integer inputTokens; diff --git a/src/main/java/com/alibaba/dashscope/aigc/videosynthesis/VideoSynthesis.java b/src/main/java/com/alibaba/dashscope/aigc/videosynthesis/VideoSynthesis.java index 8b3aca28..9ee0a47e 100644 --- a/src/main/java/com/alibaba/dashscope/aigc/videosynthesis/VideoSynthesis.java +++ b/src/main/java/com/alibaba/dashscope/aigc/videosynthesis/VideoSynthesis.java @@ -80,23 +80,7 @@ public static class AudioSetting { /** * Create ApiServiceOption * - * @return ApiServiceOption - */ - private ApiServiceOption getApiServiceOption() { - return ApiServiceOption.builder() - .protocol(Protocol.HTTP) - .httpMethod(HttpMethod.POST) - .streamingMode(StreamingMode.NONE) - .taskGroup(taskGroup) - .task(task) - .function(function) - .isAsyncTask(true) - .build(); - } - - /** - * Create ApiServiceOption - * + * @param task The task name for the API service. * @return ApiServiceOption */ private ApiServiceOption getApiServiceOption(String task) { @@ -115,7 +99,7 @@ private ApiServiceOption getApiServiceOption(String task) { public VideoSynthesis() { // only support http asyncApi = new AsynchronousApi<>(); - createServiceOptions = getApiServiceOption(); + createServiceOptions = getApiServiceOption(task); this.baseUrl = null; } @@ -127,7 +111,7 @@ public VideoSynthesis() { public VideoSynthesis(String baseUrl) { // only support http asyncApi = new AsynchronousApi<>(); - createServiceOptions = getApiServiceOption(); + createServiceOptions = getApiServiceOption(task); this.baseUrl = baseUrl; } diff --git a/src/main/java/com/alibaba/dashscope/api/SynchronizeHalfDuplexApi.java b/src/main/java/com/alibaba/dashscope/api/SynchronizeHalfDuplexApi.java index d321caf3..0de0845a 100644 --- a/src/main/java/com/alibaba/dashscope/api/SynchronizeHalfDuplexApi.java +++ b/src/main/java/com/alibaba/dashscope/api/SynchronizeHalfDuplexApi.java @@ -58,6 +58,21 @@ public DashScopeResult call(ParamT param) throws ApiException, NoApiKeyException return client.send(req); } + /** + * Call the server to get the whole result with custom service option. + * + * @param param The input param, should be the subclass of `ConversationParam`. + * @param serviceOption The custom service option for this call. + * @return The output structure, should be the subclass of `ConversationResult`. + * @throws NoApiKeyException Can not find api key + * @throws ApiException The request failed, possibly due to a network or data error. + */ + public DashScopeResult call(ParamT param, ServiceOption serviceOption) + throws ApiException, NoApiKeyException { + HalfDuplexRequest req = new HalfDuplexRequest(param, serviceOption); + return client.send(req); + } + /** * Call the server to get the result in the callback function. * @@ -72,6 +87,22 @@ public void call(ParamT param, ResultCallback callback) client.send(req, callback); } + /** + * Call the server to get the result in the callback function with custom service option. + * + * @param param The input param, should be the subclass of `Param`. + * @param callback The callback to receive response, should be the subclass of `Result`. + * @param serviceOption The custom service option for this call. + * @throws NoApiKeyException Can not find api key + * @throws ApiException The request failed, possibly due to a network or data error. + */ + public void call( + ParamT param, ResultCallback callback, ServiceOption serviceOption) + throws ApiException, NoApiKeyException { + HalfDuplexRequest req = new HalfDuplexRequest(param, serviceOption); + client.send(req, callback); + } + /** * Call the server to get the result by stream. * @@ -85,6 +116,21 @@ public Flowable streamCall(ParamT param) throws ApiException, N return client.streamOut(req); } + /** + * Call the server to get the result by stream with custom service option. + * + * @param param The input param, should be the subclass of `Param`. + * @param serviceOption The custom service option for this call. + * @return A `Flowable` of the output structure, which is the subclass of `Result`. + * @throws NoApiKeyException Can not find api key + * @throws ApiException The request failed, possibly due to a network or data error. + */ + public Flowable streamCall(ParamT param, ServiceOption serviceOption) + throws ApiException, NoApiKeyException { + HalfDuplexRequest req = new HalfDuplexRequest(param, serviceOption); + return client.streamOut(req); + } + /** * Call the server to get the result by stream. * @@ -99,6 +145,22 @@ public void streamCall(ParamT param, ResultCallback callback) client.streamOut(req, callback); } + /** + * Call the server to get the result by stream with custom service option. + * + * @param param The input param, should be the subclass of `Param`. + * @param callback The result callback. + * @param serviceOption The custom service option for this call. + * @throws NoApiKeyException Can not find api key + * @throws ApiException The request failed, possibly due to a network or data error. + */ + public void streamCall( + ParamT param, ResultCallback callback, ServiceOption serviceOption) + throws ApiException, NoApiKeyException { + HalfDuplexRequest req = new HalfDuplexRequest(param, serviceOption); + client.streamOut(req, callback); + } + public boolean close(int code, String reason) { if (client != null) { return client.close(code, reason); diff --git a/src/main/java/com/alibaba/dashscope/app/Application.java b/src/main/java/com/alibaba/dashscope/app/Application.java index b869814b..f788fe9b 100644 --- a/src/main/java/com/alibaba/dashscope/app/Application.java +++ b/src/main/java/com/alibaba/dashscope/app/Application.java @@ -12,13 +12,11 @@ import io.reactivex.Flowable; /** - * Title Ap completion calls.
- * Description App completion calls.
- * Created at 2024-02-23 15:38 + * Application completion calls. * * @since jdk8 */ -public class Application { +public final class Application { private final SynchronizeHalfDuplexApi syncApi; private final ApiServiceOption serviceOption; @@ -42,7 +40,7 @@ public Application(String baseUrl) { } /** - * app completion call for http request + * App completion call for http request * * @param param app completion params * @return app completion result @@ -61,7 +59,7 @@ public ApplicationResult call(ApplicationParam param) } /** - * app completion call for http request by sse stream + * App completion call for http request by sse stream * * @param param app completion params * @return flowable stream of app completion result @@ -88,8 +86,5 @@ private void setRequestOption(ApiServiceOption serviceOption, String resourceId) serviceOption.setTaskGroup("apps"); serviceOption.setTask(resourceId); serviceOption.setFunction("completion"); - // serviceOption.setResource("apps"); - // serviceOption.setResourceId(resourceId); - // serviceOption.setAction("completion"); } } diff --git a/src/main/java/com/alibaba/dashscope/common/ResponseFormat.java b/src/main/java/com/alibaba/dashscope/common/ResponseFormat.java index f7820bd2..26e212b4 100644 --- a/src/main/java/com/alibaba/dashscope/common/ResponseFormat.java +++ b/src/main/java/com/alibaba/dashscope/common/ResponseFormat.java @@ -2,6 +2,7 @@ package com.alibaba.dashscope.common; import com.google.gson.annotations.SerializedName; +import lombok.Builder; import lombok.Data; import lombok.experimental.SuperBuilder; @@ -12,10 +13,41 @@ public class ResponseFormat { public static final String JSON_OBJECT = "json_object"; + public static final String JSON_SCHEMA = "json_schema"; + @SerializedName("type") private Object type; + /** + * JSON schema definition when type is JSON_SCHEMA. Used to enforce structured output from the + * model. + */ + @SerializedName("json_schema") + private JsonSchema jsonSchema; + + @SuperBuilder + @Data + public static class JsonSchema { + /** A friendly name for the JSON schema. */ + private String name; + + /** The JSON schema definition as a string or object. */ + private Object schema; + + /** Whether to strictly validate the output against the schema. Default: true. */ + @Builder.Default + @SerializedName("strict") + private Boolean strict = true; + } + public static ResponseFormat from(Object type) { return ResponseFormat.builder().type(type).build(); } + + public static ResponseFormat fromJsonSchema(String name, Object schema) { + return ResponseFormat.builder() + .type(JSON_SCHEMA) + .jsonSchema(JsonSchema.builder().name(name).schema(schema).build()) + .build(); + } } diff --git a/src/main/java/com/alibaba/dashscope/embeddings/BatchTextEmbedding.java b/src/main/java/com/alibaba/dashscope/embeddings/BatchTextEmbedding.java index 76e7c5ed..542d06cf 100644 --- a/src/main/java/com/alibaba/dashscope/embeddings/BatchTextEmbedding.java +++ b/src/main/java/com/alibaba/dashscope/embeddings/BatchTextEmbedding.java @@ -16,12 +16,12 @@ import com.alibaba.dashscope.task.AsyncTaskListParam; import com.alibaba.dashscope.task.AsyncTaskListResult; -public class BatchTextEmbedding { +public final class BatchTextEmbedding { private final AsynchronousApi asyncApi; private final ApiServiceOption serviceOption; private final String baseUrl; - public final class Models { + public static class Models { public static final String TEXT_EMBEDDING_ASYNC_V1 = "text-embedding-async-v1"; public static final String TEXT_EMBEDDING_ASYNC_V2 = "text-embedding-async-v2"; } diff --git a/src/main/java/com/alibaba/dashscope/embeddings/MultiModalEmbedding.java b/src/main/java/com/alibaba/dashscope/embeddings/MultiModalEmbedding.java index 3a535451..61ca2baf 100644 --- a/src/main/java/com/alibaba/dashscope/embeddings/MultiModalEmbedding.java +++ b/src/main/java/com/alibaba/dashscope/embeddings/MultiModalEmbedding.java @@ -1,5 +1,4 @@ // Copyright (c) Alibaba, Inc. and its affiliates. - package com.alibaba.dashscope.embeddings; import com.alibaba.dashscope.api.SynchronizeHalfDuplexApi; @@ -18,7 +17,7 @@ import com.alibaba.dashscope.protocol.StreamingMode; import com.alibaba.dashscope.utils.PreprocessMessageInput; -public class MultiModalEmbedding { +public final class MultiModalEmbedding { private final SynchronizeHalfDuplexApi syncApi; private final ApiServiceOption serviceOption; @@ -28,7 +27,7 @@ public static class Models { public static final String MULTIMODAL_EMBEDDING_V1 = "multimodal-embedding-v1"; } - private ApiServiceOption defaulApiServiceOption() { + private ApiServiceOption defaultApiServiceOption() { return ApiServiceOption.builder() .protocol(Protocol.HTTP) .httpMethod(HttpMethod.POST) @@ -41,12 +40,12 @@ private ApiServiceOption defaulApiServiceOption() { } public MultiModalEmbedding() { - serviceOption = defaulApiServiceOption(); + serviceOption = defaultApiServiceOption(); syncApi = new SynchronizeHalfDuplexApi<>(serviceOption); } public MultiModalEmbedding(String baseUrl) { - serviceOption = defaulApiServiceOption(); + serviceOption = defaultApiServiceOption(); serviceOption.setBaseHttpUrl(baseUrl); syncApi = new SynchronizeHalfDuplexApi<>(serviceOption); } @@ -54,8 +53,9 @@ public MultiModalEmbedding(String baseUrl) { /** * Call the server to get the result in the callback function. * - * @param param The input param of class `GenerationParam`. - * @param callback The callback to receive response, the template class is `GenerationResult`. + * @param param The input param of class `MultiModalEmbeddingParam`. + * @param callback The callback to receive response, the template class is + * `MultiModalEmbeddingResult`. * @throws NoApiKeyException Can not find api key * @throws ApiException The request failed, possibly due to a network or data error. * @throws UploadFileException File upload failed. @@ -87,8 +87,8 @@ public void onError(Exception e) { /** * Call the server to get the whole result, only http protocol * - * @param param The input param of class `ConversationParam`. - * @return The output structure of `QWenConversationResult`. + * @param param The input param of class `MultiModalEmbeddingParam`. + * @return The output structure of `MultiModalEmbeddingResult`. * @throws NoApiKeyException Can not find api key * @throws ApiException The request failed, possibly due to a network or data error. * @throws UploadFileException File upload failed. diff --git a/src/main/java/com/alibaba/dashscope/embeddings/TextEmbedding.java b/src/main/java/com/alibaba/dashscope/embeddings/TextEmbedding.java index d5fd6767..09273071 100644 --- a/src/main/java/com/alibaba/dashscope/embeddings/TextEmbedding.java +++ b/src/main/java/com/alibaba/dashscope/embeddings/TextEmbedding.java @@ -1,5 +1,4 @@ // Copyright (c) Alibaba, Inc. and its affiliates. - package com.alibaba.dashscope.embeddings; import com.alibaba.dashscope.api.SynchronizeHalfDuplexApi; @@ -16,18 +15,18 @@ import com.alibaba.dashscope.protocol.Protocol; import com.alibaba.dashscope.protocol.StreamingMode; -public class TextEmbedding { +public final class TextEmbedding { private final SynchronizeHalfDuplexApi syncApi; private final ApiServiceOption serviceOption; - public final class Models { + public static class Models { public static final String TEXT_EMBEDDING_V1 = "text-embedding-v1"; public static final String TEXT_EMBEDDING_V2 = "text-embedding-v2"; public static final String TEXT_EMBEDDING_V3 = "text-embedding-v3"; public static final String TEXT_EMBEDDING_V4 = "text-embedding-v4"; } - private ApiServiceOption defaulApiServiceOption() { + private ApiServiceOption defaultApiServiceOption() { return ApiServiceOption.builder() .protocol(Protocol.HTTP) .httpMethod(HttpMethod.POST) @@ -40,12 +39,12 @@ private ApiServiceOption defaulApiServiceOption() { } public TextEmbedding() { - serviceOption = defaulApiServiceOption(); + serviceOption = defaultApiServiceOption(); syncApi = new SynchronizeHalfDuplexApi<>(serviceOption); } public TextEmbedding(String baseUrl) { - serviceOption = defaulApiServiceOption(); + serviceOption = defaultApiServiceOption(); serviceOption.setBaseHttpUrl(baseUrl); syncApi = new SynchronizeHalfDuplexApi<>(serviceOption); } @@ -53,8 +52,8 @@ public TextEmbedding(String baseUrl) { /** * Call the server to get the result in the callback function. * - * @param param The input param of class `GenerationParam`. - * @param callback The callback to receive response, the template class is `GenerationResult`. + * @param param The input param of class `TextEmbeddingParam`. + * @param callback The callback to receive response, the template class is `TextEmbeddingResult`. * @throws NoApiKeyException Can not find api key * @throws ApiException The request failed, possibly due to a network or data error. */ @@ -83,8 +82,8 @@ public void onError(Exception e) { /** * Call the server to get the whole result, only http protocol * - * @param param The input param of class `ConversationParam`. - * @return The output structure of `QWenConversationResult`. + * @param param The input param of class `TextEmbeddingParam`. + * @return The output structure of `TextEmbeddingResult`. * @throws NoApiKeyException Can not find api key * @throws ApiException The request failed, possibly due to a network or data error. */ diff --git a/src/main/java/com/alibaba/dashscope/multimodal/MultiModalDialog.java b/src/main/java/com/alibaba/dashscope/multimodal/MultiModalDialog.java index 64c463b3..5f6ae37a 100644 --- a/src/main/java/com/alibaba/dashscope/multimodal/MultiModalDialog.java +++ b/src/main/java/com/alibaba/dashscope/multimodal/MultiModalDialog.java @@ -1,8 +1,14 @@ +// Copyright (c) Alibaba, Inc. and its affiliates. package com.alibaba.dashscope.multimodal; import com.alibaba.dashscope.Version; import com.alibaba.dashscope.api.SynchronizeFullDuplexApi; -import com.alibaba.dashscope.common.*; +import com.alibaba.dashscope.common.DashScopeResult; +import com.alibaba.dashscope.common.Function; +import com.alibaba.dashscope.common.OutputMode; +import com.alibaba.dashscope.common.ResultCallback; +import com.alibaba.dashscope.common.Task; +import com.alibaba.dashscope.common.TaskGroup; import com.alibaba.dashscope.exception.ApiException; import com.alibaba.dashscope.exception.InputRequiredException; import com.alibaba.dashscope.exception.NoApiKeyException; diff --git a/src/main/java/com/alibaba/dashscope/multimodal/MultiModalDialogApiKeyWords.java b/src/main/java/com/alibaba/dashscope/multimodal/MultiModalDialogApiKeyWords.java index 8b1cadc0..61280bfa 100644 --- a/src/main/java/com/alibaba/dashscope/multimodal/MultiModalDialogApiKeyWords.java +++ b/src/main/java/com/alibaba/dashscope/multimodal/MultiModalDialogApiKeyWords.java @@ -1,3 +1,4 @@ +// Copyright (c) Alibaba, Inc. and its affiliates. package com.alibaba.dashscope.multimodal; /** author songsong.shao date 2025/4/25 */ diff --git a/src/main/java/com/alibaba/dashscope/multimodal/MultiModalDialogCallback.java b/src/main/java/com/alibaba/dashscope/multimodal/MultiModalDialogCallback.java index 269e756d..3eed5d9a 100644 --- a/src/main/java/com/alibaba/dashscope/multimodal/MultiModalDialogCallback.java +++ b/src/main/java/com/alibaba/dashscope/multimodal/MultiModalDialogCallback.java @@ -1,3 +1,4 @@ +// Copyright (c) Alibaba, Inc. and its affiliates. package com.alibaba.dashscope.multimodal; import com.google.gson.JsonObject; diff --git a/src/main/java/com/alibaba/dashscope/multimodal/MultiModalRequestParam.java b/src/main/java/com/alibaba/dashscope/multimodal/MultiModalRequestParam.java index fa75ebf0..98643378 100644 --- a/src/main/java/com/alibaba/dashscope/multimodal/MultiModalRequestParam.java +++ b/src/main/java/com/alibaba/dashscope/multimodal/MultiModalRequestParam.java @@ -1,3 +1,4 @@ +// Copyright (c) Alibaba, Inc. and its affiliates. package com.alibaba.dashscope.multimodal; /** author songsong.shao date 2025/4/24 */ diff --git a/src/main/java/com/alibaba/dashscope/multimodal/State.java b/src/main/java/com/alibaba/dashscope/multimodal/State.java index b701cfca..f56d821d 100644 --- a/src/main/java/com/alibaba/dashscope/multimodal/State.java +++ b/src/main/java/com/alibaba/dashscope/multimodal/State.java @@ -1,3 +1,4 @@ +// Copyright (c) Alibaba, Inc. and its affiliates. package com.alibaba.dashscope.multimodal; import lombok.Getter; diff --git a/src/main/java/com/alibaba/dashscope/multimodal/tingwu/TingWu.java b/src/main/java/com/alibaba/dashscope/multimodal/tingwu/TingWu.java index fc865c05..5b21290c 100644 --- a/src/main/java/com/alibaba/dashscope/multimodal/tingwu/TingWu.java +++ b/src/main/java/com/alibaba/dashscope/multimodal/tingwu/TingWu.java @@ -1,12 +1,17 @@ +// Copyright (c) Alibaba, Inc. and its affiliates. package com.alibaba.dashscope.multimodal.tingwu; import com.alibaba.dashscope.api.SynchronizeHalfDuplexApi; import com.alibaba.dashscope.base.HalfDuplexServiceParam; -import com.alibaba.dashscope.common.*; +import com.alibaba.dashscope.common.DashScopeResult; +import com.alibaba.dashscope.common.ResultCallback; import com.alibaba.dashscope.exception.ApiException; import com.alibaba.dashscope.exception.InputRequiredException; import com.alibaba.dashscope.exception.NoApiKeyException; -import com.alibaba.dashscope.protocol.*; +import com.alibaba.dashscope.protocol.ApiServiceOption; +import com.alibaba.dashscope.protocol.ConnectionOptions; +import com.alibaba.dashscope.protocol.HttpMethod; +import com.alibaba.dashscope.protocol.Protocol; /** The tingwu client. */ public final class TingWu { @@ -31,13 +36,14 @@ public TingWu() { public TingWu(String protocol) { serviceOption = defaultApiServiceOption(); + serviceOption.setProtocol(Protocol.of(protocol)); syncApi = new SynchronizeHalfDuplexApi<>(serviceOption); } public TingWu(String protocol, String baseUrl) { serviceOption = defaultApiServiceOption(); serviceOption.setProtocol(Protocol.of(protocol)); - if (protocol.equals(Protocol.HTTP.getValue())) { + if (Protocol.HTTP.getValue().equals(protocol)) { serviceOption.setBaseHttpUrl(baseUrl); } else { serviceOption.setBaseWebSocketUrl(baseUrl); @@ -48,7 +54,7 @@ public TingWu(String protocol, String baseUrl) { public TingWu(String protocol, String baseUrl, ConnectionOptions connectionOptions) { serviceOption = defaultApiServiceOption(); serviceOption.setProtocol(Protocol.of(protocol)); - if (protocol.equals(Protocol.HTTP.getValue())) { + if (Protocol.HTTP.getValue().equals(protocol)) { serviceOption.setBaseHttpUrl(baseUrl); } else { serviceOption.setBaseWebSocketUrl(baseUrl); @@ -60,7 +66,30 @@ public TingWu(String protocol, String baseUrl, ConnectionOptions connectionOptio public DashScopeResult call(HalfDuplexServiceParam param) throws ApiException, NoApiKeyException, InputRequiredException { param.validate(); - serviceOption.setIsSSE(false); + ApiServiceOption callOption = + ApiServiceOption.builder() + .protocol(serviceOption.getProtocol()) + .httpMethod(serviceOption.getHttpMethod()) + .isService(serviceOption.getIsService()) + .baseHttpUrl(serviceOption.getBaseHttpUrl()) + .baseWebSocketUrl(serviceOption.getBaseWebSocketUrl()) + .isSSE(false) + .build(); return syncApi.call(param); } + + /** + * Call the server to get the result in the callback function. + * + * @param param The input param. + * @param callback The callback to receive response. + * @throws NoApiKeyException Can not find api key. + * @throws ApiException The request failed, possibly due to a network or data error. + * @throws InputRequiredException Missing inputs. + */ + public void call(HalfDuplexServiceParam param, ResultCallback callback) + throws ApiException, NoApiKeyException, InputRequiredException { + param.validate(); + syncApi.call(param, callback); + } } diff --git a/src/main/java/com/alibaba/dashscope/multimodal/tingwu/TingWuParam.java b/src/main/java/com/alibaba/dashscope/multimodal/tingwu/TingWuParam.java index 546c898d..f5472ad1 100644 --- a/src/main/java/com/alibaba/dashscope/multimodal/tingwu/TingWuParam.java +++ b/src/main/java/com/alibaba/dashscope/multimodal/tingwu/TingWuParam.java @@ -1,3 +1,4 @@ +// Copyright (c) Alibaba, Inc. and its affiliates. package com.alibaba.dashscope.multimodal.tingwu; import com.alibaba.dashscope.base.HalfDuplexServiceParam; diff --git a/src/main/java/com/alibaba/dashscope/multimodal/tingwu/TingWuRealtime.java b/src/main/java/com/alibaba/dashscope/multimodal/tingwu/TingWuRealtime.java index 552edc24..1fb79461 100644 --- a/src/main/java/com/alibaba/dashscope/multimodal/tingwu/TingWuRealtime.java +++ b/src/main/java/com/alibaba/dashscope/multimodal/tingwu/TingWuRealtime.java @@ -3,7 +3,12 @@ package com.alibaba.dashscope.multimodal.tingwu; import com.alibaba.dashscope.api.SynchronizeFullDuplexApi; -import com.alibaba.dashscope.common.*; +import com.alibaba.dashscope.common.DashScopeResult; +import com.alibaba.dashscope.common.Function; +import com.alibaba.dashscope.common.OutputMode; +import com.alibaba.dashscope.common.ResultCallback; +import com.alibaba.dashscope.common.Task; +import com.alibaba.dashscope.common.TaskGroup; import com.alibaba.dashscope.exception.ApiException; import com.alibaba.dashscope.exception.InputRequiredException; import com.alibaba.dashscope.exception.NoApiKeyException; diff --git a/src/main/java/com/alibaba/dashscope/multimodal/tingwu/TingWuRealtimeCallback.java b/src/main/java/com/alibaba/dashscope/multimodal/tingwu/TingWuRealtimeCallback.java index a7344b0e..44174e4b 100644 --- a/src/main/java/com/alibaba/dashscope/multimodal/tingwu/TingWuRealtimeCallback.java +++ b/src/main/java/com/alibaba/dashscope/multimodal/tingwu/TingWuRealtimeCallback.java @@ -1,3 +1,4 @@ +// Copyright (c) Alibaba, Inc. and its affiliates. package com.alibaba.dashscope.multimodal.tingwu; import com.google.gson.JsonObject; diff --git a/src/main/java/com/alibaba/dashscope/multimodal/tingwu/TingWuRealtimeParam.java b/src/main/java/com/alibaba/dashscope/multimodal/tingwu/TingWuRealtimeParam.java index ce8791f3..4c8606b4 100644 --- a/src/main/java/com/alibaba/dashscope/multimodal/tingwu/TingWuRealtimeParam.java +++ b/src/main/java/com/alibaba/dashscope/multimodal/tingwu/TingWuRealtimeParam.java @@ -1,3 +1,4 @@ +// Copyright (c) Alibaba, Inc. and its affiliates. package com.alibaba.dashscope.multimodal.tingwu; import static com.alibaba.dashscope.multimodal.MultiModalDialogApiKeyWords.CONST_NAME_DIRECTIVE; diff --git a/src/main/java/com/alibaba/dashscope/nlp/understanding/Understanding.java b/src/main/java/com/alibaba/dashscope/nlp/understanding/Understanding.java index a4b5123b..5cd4720b 100644 --- a/src/main/java/com/alibaba/dashscope/nlp/understanding/Understanding.java +++ b/src/main/java/com/alibaba/dashscope/nlp/understanding/Understanding.java @@ -1,15 +1,25 @@ +// Copyright (c) Alibaba, Inc. and its affiliates. package com.alibaba.dashscope.nlp.understanding; import com.alibaba.dashscope.api.SynchronizeHalfDuplexApi; -import com.alibaba.dashscope.common.*; +import com.alibaba.dashscope.common.DashScopeResult; +import com.alibaba.dashscope.common.Function; +import com.alibaba.dashscope.common.OutputMode; +import com.alibaba.dashscope.common.ResultCallback; +import com.alibaba.dashscope.common.Task; +import com.alibaba.dashscope.common.TaskGroup; import com.alibaba.dashscope.exception.ApiException; import com.alibaba.dashscope.exception.InputRequiredException; import com.alibaba.dashscope.exception.NoApiKeyException; -import com.alibaba.dashscope.protocol.*; +import com.alibaba.dashscope.protocol.ApiServiceOption; +import com.alibaba.dashscope.protocol.ConnectionOptions; +import com.alibaba.dashscope.protocol.HttpMethod; +import com.alibaba.dashscope.protocol.Protocol; +import com.alibaba.dashscope.protocol.StreamingMode; import lombok.extern.slf4j.Slf4j; @Slf4j -public class Understanding { +public final class Understanding { private final SynchronizeHalfDuplexApi syncApi; private final ApiServiceOption serviceOption; @@ -21,13 +31,12 @@ private ApiServiceOption defaultApiServiceOption() { return ApiServiceOption.builder() .protocol(Protocol.HTTP) .httpMethod(HttpMethod.POST) - .streamingMode(StreamingMode.OUT) + .streamingMode(StreamingMode.NONE) .outputMode(OutputMode.ACCUMULATE) .taskGroup(TaskGroup.NLP.getValue()) .task(Task.NLU.getValue()) .function(Function.UNDERSTANDING.getValue()) .isSSE(false) - .streamingMode(StreamingMode.NONE) .build(); } @@ -53,6 +62,17 @@ public Understanding(String protocol, String baseUrl) { syncApi = new SynchronizeHalfDuplexApi<>(serviceOption); } + public Understanding(String protocol, String baseUrl, ConnectionOptions connectionOptions) { + serviceOption = defaultApiServiceOption(); + serviceOption.setProtocol(Protocol.of(protocol)); + if (Protocol.HTTP.getValue().equals(protocol)) { + serviceOption.setBaseHttpUrl(baseUrl); + } else { + serviceOption.setBaseWebSocketUrl(baseUrl); + } + syncApi = new SynchronizeHalfDuplexApi<>(connectionOptions, serviceOption); + } + /** * Call the server to get the whole result, only http protocol * diff --git a/src/main/java/com/alibaba/dashscope/nlp/understanding/UnderstandingParam.java b/src/main/java/com/alibaba/dashscope/nlp/understanding/UnderstandingParam.java index 21df75b5..5c72bfcc 100644 --- a/src/main/java/com/alibaba/dashscope/nlp/understanding/UnderstandingParam.java +++ b/src/main/java/com/alibaba/dashscope/nlp/understanding/UnderstandingParam.java @@ -1,3 +1,4 @@ +// Copyright (c) Alibaba, Inc. and its affiliates. package com.alibaba.dashscope.nlp.understanding; import com.alibaba.dashscope.base.HalfDuplexServiceParam; diff --git a/src/main/java/com/alibaba/dashscope/rerank/TextReRank.java b/src/main/java/com/alibaba/dashscope/rerank/TextReRank.java index 5e56a470..6e43c84e 100644 --- a/src/main/java/com/alibaba/dashscope/rerank/TextReRank.java +++ b/src/main/java/com/alibaba/dashscope/rerank/TextReRank.java @@ -2,11 +2,18 @@ package com.alibaba.dashscope.rerank; import com.alibaba.dashscope.api.SynchronizeHalfDuplexApi; -import com.alibaba.dashscope.common.*; +import com.alibaba.dashscope.common.DashScopeResult; +import com.alibaba.dashscope.common.OutputMode; +import com.alibaba.dashscope.common.ResultCallback; +import com.alibaba.dashscope.common.TaskGroup; import com.alibaba.dashscope.exception.ApiException; import com.alibaba.dashscope.exception.InputRequiredException; import com.alibaba.dashscope.exception.NoApiKeyException; -import com.alibaba.dashscope.protocol.*; +import com.alibaba.dashscope.protocol.ApiServiceOption; +import com.alibaba.dashscope.protocol.ConnectionOptions; +import com.alibaba.dashscope.protocol.HttpMethod; +import com.alibaba.dashscope.protocol.Protocol; +import com.alibaba.dashscope.protocol.StreamingMode; import lombok.extern.slf4j.Slf4j; @Slf4j @@ -72,11 +79,62 @@ public TextReRank(String protocol, String baseUrl, ConnectionOptions connectionO * @throws NoApiKeyException Can not find api key * @throws ApiException The request failed, possibly due to a network or data error. */ + /** Creates a copy of the shared serviceOption for thread-safe per-call usage. */ + private ApiServiceOption copyServiceOption() { + return ApiServiceOption.builder() + .protocol(serviceOption.getProtocol()) + .httpMethod(serviceOption.getHttpMethod()) + .streamingMode(serviceOption.getStreamingMode()) + .outputMode(serviceOption.getOutputMode()) + .taskGroup(serviceOption.getTaskGroup()) + .task(serviceOption.getTask()) + .function(serviceOption.getFunction()) + .baseHttpUrl(serviceOption.getBaseHttpUrl()) + .baseWebSocketUrl(serviceOption.getBaseWebSocketUrl()) + .build(); + } + public TextReRankResult call(TextReRankParam param) throws ApiException, NoApiKeyException, InputRequiredException { param.validate(); - serviceOption.setIsSSE(false); - serviceOption.setStreamingMode(StreamingMode.NONE); - return TextReRankResult.fromDashScopeResult(syncApi.call(param)); + ApiServiceOption callOption = copyServiceOption(); + callOption.setIsSSE(false); + callOption.setStreamingMode(StreamingMode.NONE); + return TextReRankResult.fromDashScopeResult(syncApi.call(param, callOption)); + } + + /** + * Call the server to get the result in the callback function. + * + * @param param The input param of class `TextReRankParam`. + * @param callback The callback to receive response, the template class is `TextReRankResult`. + * @throws NoApiKeyException Can not find api key + * @throws ApiException The request failed, possibly due to a network or data error. + */ + public void call(TextReRankParam param, ResultCallback callback) + throws ApiException, NoApiKeyException, InputRequiredException { + param.validate(); + ApiServiceOption callOption = copyServiceOption(); + callOption.setIsSSE(false); + callOption.setStreamingMode(StreamingMode.NONE); + syncApi.call( + param, + new ResultCallback() { + @Override + public void onEvent(DashScopeResult message) { + callback.onEvent(TextReRankResult.fromDashScopeResult(message)); + } + + @Override + public void onComplete() { + callback.onComplete(); + } + + @Override + public void onError(Exception e) { + callback.onError(e); + } + }, + callOption); } } From 1cee9e4d6b499ed11df71a5e0e04027b30d52464 Mon Sep 17 00:00:00 2001 From: "zhansheng.lzs" Date: Thu, 4 Jun 2026 15:58:06 +0800 Subject: [PATCH 2/8] Fix bugs in code optimization commit and add missing classes - TingWu: fix dead callOption variable, restore serviceOption.setIsSSE(false) - TextReRank: move misplaced Javadoc back to call() method - ImageSynthesis: restore ApiException wrapping for UploadFileException - Add missing Plugins, StreamOptions, StreamingMerger classes Co-Authored-By: Claude Opus 4.6 --- samples/ImageSynthesisUsage.java | 11 +- .../aigc/imagesynthesis/ImageSynthesis.java | 13 +- .../com/alibaba/dashscope/common/Plugins.java | 23 +++ .../dashscope/common/StreamOptions.java | 13 ++ .../dashscope/multimodal/tingwu/TingWu.java | 10 +- .../alibaba/dashscope/rerank/TextReRank.java | 16 +-- .../dashscope/utils/StreamingMerger.java | 132 ++++++++++++++++++ 7 files changed, 188 insertions(+), 30 deletions(-) create mode 100644 src/main/java/com/alibaba/dashscope/common/Plugins.java create mode 100644 src/main/java/com/alibaba/dashscope/common/StreamOptions.java create mode 100644 src/main/java/com/alibaba/dashscope/utils/StreamingMerger.java diff --git a/samples/ImageSynthesisUsage.java b/samples/ImageSynthesisUsage.java index 0ac1db12..ae00066d 100644 --- a/samples/ImageSynthesisUsage.java +++ b/samples/ImageSynthesisUsage.java @@ -7,12 +7,11 @@ import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesisParam; import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesisResult; import com.alibaba.dashscope.exception.ApiException; -import com.alibaba.dashscope.exception.InputRequiredException; import com.alibaba.dashscope.exception.NoApiKeyException; import com.alibaba.dashscope.task.AsyncTaskListParam; public class ImageSynthesisUsage { - public static void basicCall() throws ApiException, NoApiKeyException, InputRequiredException { + public static void basicCall() throws ApiException, NoApiKeyException { // create with image2image, 参考文档(image2image|text2image) ImageSynthesis is = new ImageSynthesis(); ImageSynthesisParam param = @@ -29,7 +28,7 @@ public static void basicCall() throws ApiException, NoApiKeyException, InputRequ System.out.println(result); } - public static void synCall() throws ApiException, NoApiKeyException, InputRequiredException { + public static void synCall() throws ApiException, NoApiKeyException { ImageSynthesis is = new ImageSynthesis(); ImageSynthesisParam param = ImageSynthesisParam.builder() @@ -44,14 +43,14 @@ public static void synCall() throws ApiException, NoApiKeyException, InputRequir System.out.println(result); } - public static void listTask() throws ApiException, NoApiKeyException, InputRequiredException { + public static void listTask() throws ApiException, NoApiKeyException { ImageSynthesis is = new ImageSynthesis(); AsyncTaskListParam param = AsyncTaskListParam.builder().build(); ImageSynthesisListResult result = is.list(param); System.out.println(result); } - public void fetchTask() throws ApiException, NoApiKeyException, InputRequiredException { + public void fetchTask() throws ApiException, NoApiKeyException { String taskId = "your task id"; ImageSynthesis is = new ImageSynthesis(); // If set DASHSCOPE_API_KEY environment variable, apiKey can null. @@ -65,7 +64,7 @@ public static void main(String[] args){ // basicCall(); synCall(); //listTask(); - }catch(ApiException|NoApiKeyException|InputRequiredException e){ + }catch(ApiException|NoApiKeyException e){ System.out.println(e.getMessage()); } System.exit(0); diff --git a/src/main/java/com/alibaba/dashscope/aigc/imagesynthesis/ImageSynthesis.java b/src/main/java/com/alibaba/dashscope/aigc/imagesynthesis/ImageSynthesis.java index 8e4825b3..68256bb7 100644 --- a/src/main/java/com/alibaba/dashscope/aigc/imagesynthesis/ImageSynthesis.java +++ b/src/main/java/com/alibaba/dashscope/aigc/imagesynthesis/ImageSynthesis.java @@ -6,7 +6,6 @@ import com.alibaba.dashscope.base.HalfDuplexServiceParam; import com.alibaba.dashscope.common.DashScopeResult; import com.alibaba.dashscope.exception.ApiException; -import com.alibaba.dashscope.exception.InputRequiredException; import com.alibaba.dashscope.exception.NoApiKeyException; import com.alibaba.dashscope.exception.UploadFileException; import com.alibaba.dashscope.protocol.ApiServiceOption; @@ -112,12 +111,12 @@ public ImageSynthesis(String task, String baseUrl) { } public ImageSynthesisResult asyncCall(ImageSynthesisParam param) - throws ApiException, NoApiKeyException, InputRequiredException { + throws ApiException, NoApiKeyException { // add local file support try { param.checkAndUpload(); } catch (UploadFileException e) { - throw new InputRequiredException(e.getMessage()); + throw new ApiException(e); } ApiServiceOption serviceOption = createServiceOptions; if (param.getModel().contains("imageedit") || param.getModel().contains("wan2.5-i2i")) { @@ -131,12 +130,12 @@ public ImageSynthesisResult asyncCall(ImageSynthesisParam param) * models will result in an error,More raw image models may be added for use later */ public ImageSynthesisResult syncCall(ImageSynthesisParam param) - throws ApiException, NoApiKeyException, InputRequiredException { + throws ApiException, NoApiKeyException { // add local file support try { param.checkAndUpload(); } catch (UploadFileException e) { - throw new InputRequiredException(e.getMessage()); + throw new ApiException(e); } ApiServiceOption serviceOption = createServiceOptions; serviceOption.setIsAsyncTask(false); @@ -152,12 +151,12 @@ public ImageSynthesisResult syncCall(ImageSynthesisParam param) * @throws ApiException The request failed, possibly due to a network or data error. */ public ImageSynthesisResult call(ImageSynthesisParam param) - throws ApiException, NoApiKeyException, InputRequiredException { + throws ApiException, NoApiKeyException { // add local file support try { param.checkAndUpload(); } catch (UploadFileException e) { - throw new InputRequiredException(e.getMessage()); + throw new ApiException(e); } ApiServiceOption serviceOption = createServiceOptions; if (param.getModel().contains("imageedit") || param.getModel().contains("wan2.5-i2i")) { diff --git a/src/main/java/com/alibaba/dashscope/common/Plugins.java b/src/main/java/com/alibaba/dashscope/common/Plugins.java new file mode 100644 index 00000000..4f3ebe6c --- /dev/null +++ b/src/main/java/com/alibaba/dashscope/common/Plugins.java @@ -0,0 +1,23 @@ +// Copyright (c) Alibaba, Inc. and its affiliates. +package com.alibaba.dashscope.common; + +import com.google.gson.annotations.SerializedName; +import lombok.Data; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +public class Plugins { + + @Data + public static class Search { + @SerializedName("count") + private Integer count; + + @SerializedName("strategy") + private String strategy; + } + + @SerializedName("search") + private Search search; +} diff --git a/src/main/java/com/alibaba/dashscope/common/StreamOptions.java b/src/main/java/com/alibaba/dashscope/common/StreamOptions.java new file mode 100644 index 00000000..b3910fe2 --- /dev/null +++ b/src/main/java/com/alibaba/dashscope/common/StreamOptions.java @@ -0,0 +1,13 @@ +// Copyright (c) Alibaba, Inc. and its affiliates. +package com.alibaba.dashscope.common; + +import com.google.gson.annotations.SerializedName; +import lombok.Data; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +public class StreamOptions { + @SerializedName("include_usage") + private Boolean includeUsage; +} diff --git a/src/main/java/com/alibaba/dashscope/multimodal/tingwu/TingWu.java b/src/main/java/com/alibaba/dashscope/multimodal/tingwu/TingWu.java index 5b21290c..b8e7a214 100644 --- a/src/main/java/com/alibaba/dashscope/multimodal/tingwu/TingWu.java +++ b/src/main/java/com/alibaba/dashscope/multimodal/tingwu/TingWu.java @@ -66,15 +66,7 @@ public TingWu(String protocol, String baseUrl, ConnectionOptions connectionOptio public DashScopeResult call(HalfDuplexServiceParam param) throws ApiException, NoApiKeyException, InputRequiredException { param.validate(); - ApiServiceOption callOption = - ApiServiceOption.builder() - .protocol(serviceOption.getProtocol()) - .httpMethod(serviceOption.getHttpMethod()) - .isService(serviceOption.getIsService()) - .baseHttpUrl(serviceOption.getBaseHttpUrl()) - .baseWebSocketUrl(serviceOption.getBaseWebSocketUrl()) - .isSSE(false) - .build(); + serviceOption.setIsSSE(false); return syncApi.call(param); } diff --git a/src/main/java/com/alibaba/dashscope/rerank/TextReRank.java b/src/main/java/com/alibaba/dashscope/rerank/TextReRank.java index 6e43c84e..e9520aee 100644 --- a/src/main/java/com/alibaba/dashscope/rerank/TextReRank.java +++ b/src/main/java/com/alibaba/dashscope/rerank/TextReRank.java @@ -71,14 +71,6 @@ public TextReRank(String protocol, String baseUrl, ConnectionOptions connectionO syncApi = new SynchronizeHalfDuplexApi<>(connectionOptions, serviceOption); } - /** - * Call the server to get the whole result. - * - * @param param The input param of class `TextReRankParam`. - * @return The output structure of `TextReRankResult`. - * @throws NoApiKeyException Can not find api key - * @throws ApiException The request failed, possibly due to a network or data error. - */ /** Creates a copy of the shared serviceOption for thread-safe per-call usage. */ private ApiServiceOption copyServiceOption() { return ApiServiceOption.builder() @@ -94,6 +86,14 @@ private ApiServiceOption copyServiceOption() { .build(); } + /** + * Call the server to get the whole result. + * + * @param param The input param of class `TextReRankParam`. + * @return The output structure of `TextReRankResult`. + * @throws NoApiKeyException Can not find api key + * @throws ApiException The request failed, possibly due to a network or data error. + */ public TextReRankResult call(TextReRankParam param) throws ApiException, NoApiKeyException, InputRequiredException { param.validate(); diff --git a/src/main/java/com/alibaba/dashscope/utils/StreamingMerger.java b/src/main/java/com/alibaba/dashscope/utils/StreamingMerger.java new file mode 100644 index 00000000..fc3d3be8 --- /dev/null +++ b/src/main/java/com/alibaba/dashscope/utils/StreamingMerger.java @@ -0,0 +1,132 @@ +// Copyright (c) Alibaba, Inc. and its affiliates. +package com.alibaba.dashscope.utils; + +import com.alibaba.dashscope.tools.ToolCallBase; +import com.alibaba.dashscope.tools.ToolCallFunction; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public final class StreamingMerger { + + private StreamingMerger() {} + + public static void clearAccumulatedData(ThreadLocal> accumulatedDataMap) { + accumulatedDataMap.get().clear(); + accumulatedDataMap.remove(); + } + + public static void mergeTextContent( + List> currentContent, List> accumulatedContent) { + for (Map contentItem : currentContent) { + if (contentItem.containsKey("text")) { + String textValue = (String) contentItem.get("text"); + if (textValue != null && !textValue.isEmpty()) { + Map accumulatedTextItem = null; + for (Map accItem : accumulatedContent) { + if (accItem.containsKey("text")) { + accumulatedTextItem = accItem; + break; + } + } + + if (accumulatedTextItem == null) { + accumulatedTextItem = new HashMap<>(); + accumulatedTextItem.put("text", textValue); + accumulatedContent.add(accumulatedTextItem); + } else { + String existingText = (String) accumulatedTextItem.get("text"); + if (existingText == null) { + existingText = ""; + } + accumulatedTextItem.put("text", existingText + textValue); + } + } + } + } + } + + public static void mergeToolCalls( + List currentToolCalls, List accumulatedToolCalls) { + for (ToolCallBase currentCall : currentToolCalls) { + if (currentCall == null || currentCall.getIndex() == null) { + continue; + } + + int index = currentCall.getIndex(); + + ToolCallBase existingCall = null; + for (ToolCallBase accCall : accumulatedToolCalls) { + if (accCall != null && accCall.getIndex() != null && accCall.getIndex().equals(index)) { + existingCall = accCall; + break; + } + } + + if (existingCall instanceof ToolCallFunction && currentCall instanceof ToolCallFunction) { + ToolCallFunction existingFunctionCall = (ToolCallFunction) existingCall; + ToolCallFunction currentFunctionCall = (ToolCallFunction) currentCall; + + if (currentFunctionCall.getFunction() != null) { + if (existingFunctionCall.getFunction() == null) { + existingFunctionCall.setFunction(existingFunctionCall.new CallFunction()); + } + + if (currentFunctionCall.getFunction().getArguments() != null) { + String existingArguments = existingFunctionCall.getFunction().getArguments(); + if (existingArguments == null) { + existingArguments = ""; + } + String currentArguments = currentFunctionCall.getFunction().getArguments(); + existingFunctionCall.getFunction().setArguments(existingArguments + currentArguments); + } + + if (currentFunctionCall.getFunction().getName() != null) { + String existingName = existingFunctionCall.getFunction().getName(); + if (existingName == null) { + existingName = ""; + } + String currentName = currentFunctionCall.getFunction().getName(); + existingFunctionCall.getFunction().setName(existingName + currentName); + } + + if (currentFunctionCall.getFunction().getOutput() != null) { + existingFunctionCall + .getFunction() + .setOutput(currentFunctionCall.getFunction().getOutput()); + } + } + + if (currentFunctionCall.getIndex() != null) { + existingFunctionCall.setIndex(currentFunctionCall.getIndex()); + } + if (currentFunctionCall.getId() != null && !currentFunctionCall.getId().isEmpty()) { + existingFunctionCall.setId(currentFunctionCall.getId()); + } + if (currentFunctionCall.getType() != null) { + existingFunctionCall.setType(currentFunctionCall.getType()); + } + } else { + if (currentCall instanceof ToolCallFunction) { + ToolCallFunction currentFunctionCall = (ToolCallFunction) currentCall; + ToolCallFunction newFunctionCall = new ToolCallFunction(); + newFunctionCall.setIndex(currentFunctionCall.getIndex()); + newFunctionCall.setId(currentFunctionCall.getId()); + newFunctionCall.setType(currentFunctionCall.getType()); + + if (currentFunctionCall.getFunction() != null) { + ToolCallFunction.CallFunction newCallFunction = newFunctionCall.new CallFunction(); + newCallFunction.setName(currentFunctionCall.getFunction().getName()); + newCallFunction.setArguments(currentFunctionCall.getFunction().getArguments()); + newCallFunction.setOutput(currentFunctionCall.getFunction().getOutput()); + newFunctionCall.setFunction(newCallFunction); + } + + accumulatedToolCalls.add(newFunctionCall); + } else { + accumulatedToolCalls.add(currentCall); + } + } + } + } +} From 6b8194ecf7694c09cbe7489b622cc8d2dea92131 Mon Sep 17 00:00:00 2001 From: "zhansheng.lzs" Date: Thu, 4 Jun 2026 16:49:47 +0800 Subject: [PATCH 3/8] Update version to 2.22.21 in pom.xml Co-Authored-By: Claude Opus 4.6 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 55decafe..534a5f30 100644 --- a/pom.xml +++ b/pom.xml @@ -40,7 +40,7 @@ DashScope Java SDK com.alibaba dashscope-sdk-java - 2.22.20 + 2.22.21 8 From 37fb97795d332411caac5dbec14366e7014c440c Mon Sep 17 00:00:00 2001 From: "zhansheng.lzs" Date: Thu, 4 Jun 2026 17:09:08 +0800 Subject: [PATCH 4/8] Fix incrementalOutput default value to prevent unintended stream merging Revert incrementalOutput default from false back to null. The false default caused modifyIncrementalOutput to trigger stream merging unexpectedly, breaking QVQ streaming where reasoningContent and content should not coexist in intermediate chunks. Co-Authored-By: Claude Opus 4.6 --- .../multimodalconversation/MultiModalConversationParam.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/alibaba/dashscope/aigc/multimodalconversation/MultiModalConversationParam.java b/src/main/java/com/alibaba/dashscope/aigc/multimodalconversation/MultiModalConversationParam.java index 95bb5c55..304859c1 100644 --- a/src/main/java/com/alibaba/dashscope/aigc/multimodalconversation/MultiModalConversationParam.java +++ b/src/main/java/com/alibaba/dashscope/aigc/multimodalconversation/MultiModalConversationParam.java @@ -110,7 +110,7 @@ public class MultiModalConversationParam extends HalfDuplexServiceParam { * apple * */ - @Builder.Default private Boolean incrementalOutput = false; + @Builder.Default private Boolean incrementalOutput; /** Output format of the model including "text" and "audio". Default value: ["text"] */ private List modalities; From a0d52dcb1dc317f8e6a672b80038d91c8e046e13 Mon Sep 17 00:00:00 2001 From: kevin Date: Fri, 5 Jun 2026 17:02:40 +0800 Subject: [PATCH 5/8] =?UTF-8?q?fix=20Generation=E3=80=81ImageGeneration?= =?UTF-8?q?=E3=80=81MultiModelConversation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dashscope/aigc/generation/Generation.java | 55 +++++++++---------- .../aigc/imagegeneration/ImageGeneration.java | 39 ++++++------- .../MultiModalConversation.java | 38 ++++++------- 3 files changed, 59 insertions(+), 73 deletions(-) diff --git a/src/main/java/com/alibaba/dashscope/aigc/generation/Generation.java b/src/main/java/com/alibaba/dashscope/aigc/generation/Generation.java index 271733c2..a2ab2dac 100644 --- a/src/main/java/com/alibaba/dashscope/aigc/generation/Generation.java +++ b/src/main/java/com/alibaba/dashscope/aigc/generation/Generation.java @@ -33,8 +33,6 @@ public final class Generation { private final SynchronizeHalfDuplexApi syncApi; private final ApiServiceOption serviceOption; - private final ThreadLocal> accumulatedDataMap = - ThreadLocal.withInitial(HashMap::new); public static class Models { /** @deprecated use QWEN_TURBO instead */ @@ -189,23 +187,22 @@ public Flowable streamCall(HalfDuplexServiceParam param) ApiServiceOption callOption = copyServiceOption(); callOption.setIsSSE(true); callOption.setStreamingMode(StreamingMode.OUT); - return syncApi - .streamCall(param, callOption) - .map(GenerationResult::fromDashScopeResult) - .flatMap( - result -> { - GenerationResult merged = mergeSingleResponse(result, toMergeResponse, param); - if (merged == null) { - return Flowable.empty(); - } - return Flowable.just(merged); - }) - .doFinally( - () -> { - if (toMergeResponse) { - StreamingMerger.clearAccumulatedData(accumulatedDataMap); - } - }); + return Flowable.defer( + () -> { + Map accumulatedData = new HashMap<>(); + return syncApi + .streamCall(param, callOption) + .map(GenerationResult::fromDashScopeResult) + .flatMap( + result -> { + GenerationResult merged = + mergeSingleResponse(result, toMergeResponse, param, accumulatedData); + if (merged == null) { + return Flowable.empty(); + } + return Flowable.just(merged); + }); + }); } public void streamCall(HalfDuplexServiceParam param, ResultCallback callback) @@ -223,13 +220,15 @@ public void streamCall(HalfDuplexServiceParam param, ResultCallback accumulatedData = new HashMap<>(); syncApi.streamCall( param, new ResultCallback() { @Override public void onEvent(DashScopeResult msg) { GenerationResult result = GenerationResult.fromDashScopeResult(msg); - GenerationResult mergedResult = mergeSingleResponse(result, toMergeResponse, param); + GenerationResult mergedResult = + mergeSingleResponse(result, toMergeResponse, param, accumulatedData); if (mergedResult != null) { callback.onEvent(mergedResult); } @@ -237,17 +236,13 @@ public void onEvent(DashScopeResult msg) { @Override public void onComplete() { - if (toMergeResponse) { - StreamingMerger.clearAccumulatedData(accumulatedDataMap); - } + accumulatedData.clear(); callback.onComplete(); } @Override public void onError(Exception e) { - if (toMergeResponse) { - StreamingMerger.clearAccumulatedData(accumulatedDataMap); - } + accumulatedData.clear(); callback.onError(e); } }, @@ -285,16 +280,18 @@ private boolean modifyIncrementalOutput(HalfDuplexServiceParam param) { * @param result The GenerationResult to merge * @param toMergeResponse Whether to perform merging (based on original incrementalOutput setting) * @param param The HalfDuplexServiceParam to get n parameter + * @param accumulatedData The per-stream accumulated data * @return The merged GenerationResult, or null if should be filtered out */ private GenerationResult mergeSingleResponse( - GenerationResult result, boolean toMergeResponse, HalfDuplexServiceParam param) { + GenerationResult result, + boolean toMergeResponse, + HalfDuplexServiceParam param, + Map accumulatedData) { if (!toMergeResponse || result == null || result.getOutput() == null) { return result; } - Map accumulatedData = accumulatedDataMap.get(); - // Get n parameter Integer n = null; if (param instanceof GenerationParam) { diff --git a/src/main/java/com/alibaba/dashscope/aigc/imagegeneration/ImageGeneration.java b/src/main/java/com/alibaba/dashscope/aigc/imagegeneration/ImageGeneration.java index 7fc0be78..a2be4b5d 100644 --- a/src/main/java/com/alibaba/dashscope/aigc/imagegeneration/ImageGeneration.java +++ b/src/main/java/com/alibaba/dashscope/aigc/imagegeneration/ImageGeneration.java @@ -40,8 +40,6 @@ public final class ImageGeneration { private final ApiServiceOption serviceOption; private final String baseUrl; - private final ThreadLocal> accumulatedDataMap = - ThreadLocal.withInitial(HashMap::new); public static class Models { public static final String WanX2_6_T2I = "wan2.6-t2i"; @@ -267,16 +265,14 @@ public Flowable streamCall(ImageGenerationParam param) callOption.setStreamingMode(StreamingMode.OUT); callOption.setTask(Task.MULTIMODAL_GENERATION.getValue()); preprocessInput(param); - return syncApi - .streamCall(param, callOption) - .map(ImageGenerationResult::fromDashScopeResult) - .map(result -> mergeSingleResponse(result, toMergeResponse)) - .doFinally( - () -> { - if (toMergeResponse) { - StreamingMerger.clearAccumulatedData(accumulatedDataMap); - } - }); + return Flowable.defer( + () -> { + Map accumulatedData = new HashMap<>(); + return syncApi + .streamCall(param, callOption) + .map(ImageGenerationResult::fromDashScopeResult) + .map(result -> mergeSingleResponse(result, toMergeResponse, accumulatedData)); + }); } /** @@ -306,13 +302,15 @@ public void streamCall(ImageGenerationParam param, ResultCallback accumulatedData = new HashMap<>(); syncApi.streamCall( param, new ResultCallback() { @Override public void onEvent(DashScopeResult msg) { ImageGenerationResult result = ImageGenerationResult.fromDashScopeResult(msg); - ImageGenerationResult mergedResult = mergeSingleResponse(result, toMergeResponse); + ImageGenerationResult mergedResult = + mergeSingleResponse(result, toMergeResponse, accumulatedData); if (mergedResult != null) { callback.onEvent(mergedResult); } @@ -320,17 +318,13 @@ public void onEvent(DashScopeResult msg) { @Override public void onComplete() { - if (toMergeResponse) { - StreamingMerger.clearAccumulatedData(accumulatedDataMap); - } + accumulatedData.clear(); callback.onComplete(); } @Override public void onError(Exception e) { - if (toMergeResponse) { - StreamingMerger.clearAccumulatedData(accumulatedDataMap); - } + accumulatedData.clear(); callback.onError(e); } }, @@ -385,16 +379,17 @@ private boolean modifyIncrementalOutput(ImageGenerationParam param) { * * @param result The ImageGenerationResult to merge * @param toMergeResponse Whether to perform merging (based on original incrementalOutput setting) + * @param accumulatedData The per-stream accumulated data * @return The merged ImageGenerationResult */ private ImageGenerationResult mergeSingleResponse( - ImageGenerationResult result, boolean toMergeResponse) { + ImageGenerationResult result, + boolean toMergeResponse, + Map accumulatedData) { if (!toMergeResponse || result == null || result.getOutput() == null) { return result; } - Map accumulatedData = accumulatedDataMap.get(); - // Handle choices format: output.choices[].message.content if (result.getOutput().getChoices() != null) { List choices = result.getOutput().getChoices(); diff --git a/src/main/java/com/alibaba/dashscope/aigc/multimodalconversation/MultiModalConversation.java b/src/main/java/com/alibaba/dashscope/aigc/multimodalconversation/MultiModalConversation.java index 9bafe611..84eebcca 100644 --- a/src/main/java/com/alibaba/dashscope/aigc/multimodalconversation/MultiModalConversation.java +++ b/src/main/java/com/alibaba/dashscope/aigc/multimodalconversation/MultiModalConversation.java @@ -38,8 +38,6 @@ public final class MultiModalConversation { private final SynchronizeHalfDuplexApi syncApi; private final ApiServiceOption serviceOption; - private final ThreadLocal> accumulatedDataMap = - ThreadLocal.withInitial(HashMap::new); public static class Models { public static final String QWEN_VL_CHAT_V1 = "qwen-vl-chat-v1"; @@ -184,16 +182,14 @@ public Flowable streamCall(MultiModalConversationP callOption.setIsSSE(true); callOption.setStreamingMode(StreamingMode.OUT); preprocessInput(param); - return syncApi - .streamCall(param, callOption) - .map(MultiModalConversationResult::fromDashScopeResult) - .map(result -> mergeSingleResponse(result, toMergeResponse)) - .doFinally( - () -> { - if (toMergeResponse) { - StreamingMerger.clearAccumulatedData(accumulatedDataMap); - } - }); + return Flowable.defer( + () -> { + Map accumulatedData = new HashMap<>(); + return syncApi + .streamCall(param, callOption) + .map(MultiModalConversationResult::fromDashScopeResult) + .map(result -> mergeSingleResponse(result, toMergeResponse, accumulatedData)); + }); } /** @@ -223,6 +219,7 @@ public void streamCall( callOption.setIsSSE(true); callOption.setStreamingMode(StreamingMode.OUT); preprocessInput(param); + Map accumulatedData = new HashMap<>(); syncApi.streamCall( param, new ResultCallback() { @@ -231,7 +228,7 @@ public void onEvent(DashScopeResult msg) { MultiModalConversationResult result = MultiModalConversationResult.fromDashScopeResult(msg); MultiModalConversationResult mergedResult = - mergeSingleResponse(result, toMergeResponse); + mergeSingleResponse(result, toMergeResponse, accumulatedData); if (mergedResult != null) { callback.onEvent(mergedResult); } @@ -239,17 +236,13 @@ public void onEvent(DashScopeResult msg) { @Override public void onComplete() { - if (toMergeResponse) { - StreamingMerger.clearAccumulatedData(accumulatedDataMap); - } + accumulatedData.clear(); callback.onComplete(); } @Override public void onError(Exception e) { - if (toMergeResponse) { - StreamingMerger.clearAccumulatedData(accumulatedDataMap); - } + accumulatedData.clear(); callback.onError(e); } }, @@ -319,16 +312,17 @@ private boolean modifyIncrementalOutput(MultiModalConversationParam param) { * * @param result The MultiModalConversationResult to merge * @param toMergeResponse Whether to perform merging (based on original incrementalOutput setting) + * @param accumulatedData The per-stream accumulated data * @return The merged MultiModalConversationResult */ private MultiModalConversationResult mergeSingleResponse( - MultiModalConversationResult result, boolean toMergeResponse) { + MultiModalConversationResult result, + boolean toMergeResponse, + Map accumulatedData) { if (!toMergeResponse || result == null || result.getOutput() == null) { return result; } - Map accumulatedData = accumulatedDataMap.get(); - // Handle choices format: output.choices[].message.content if (result.getOutput().getChoices() != null) { List choices = result.getOutput().getChoices(); From e264a79734e8dccab31cb1650820fce06729837e Mon Sep 17 00:00:00 2001 From: kevin Date: Fri, 5 Jun 2026 17:37:03 +0800 Subject: [PATCH 6/8] =?UTF-8?q?fix=20Generation=E3=80=81ImageGeneration?= =?UTF-8?q?=E3=80=81MultiModelConversation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/alibaba/dashscope/aigc/generation/Generation.java | 3 ++- .../dashscope/aigc/imagegeneration/ImageGeneration.java | 3 ++- .../aigc/multimodalconversation/MultiModalConversation.java | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/alibaba/dashscope/aigc/generation/Generation.java b/src/main/java/com/alibaba/dashscope/aigc/generation/Generation.java index a2ab2dac..b1dadadf 100644 --- a/src/main/java/com/alibaba/dashscope/aigc/generation/Generation.java +++ b/src/main/java/com/alibaba/dashscope/aigc/generation/Generation.java @@ -201,7 +201,8 @@ public Flowable streamCall(HalfDuplexServiceParam param) return Flowable.empty(); } return Flowable.just(merged); - }); + }) + .doFinally(accumulatedData::clear); }); } diff --git a/src/main/java/com/alibaba/dashscope/aigc/imagegeneration/ImageGeneration.java b/src/main/java/com/alibaba/dashscope/aigc/imagegeneration/ImageGeneration.java index a2be4b5d..d1993ffb 100644 --- a/src/main/java/com/alibaba/dashscope/aigc/imagegeneration/ImageGeneration.java +++ b/src/main/java/com/alibaba/dashscope/aigc/imagegeneration/ImageGeneration.java @@ -271,7 +271,8 @@ public Flowable streamCall(ImageGenerationParam param) return syncApi .streamCall(param, callOption) .map(ImageGenerationResult::fromDashScopeResult) - .map(result -> mergeSingleResponse(result, toMergeResponse, accumulatedData)); + .map(result -> mergeSingleResponse(result, toMergeResponse, accumulatedData)) + .doFinally(accumulatedData::clear); }); } diff --git a/src/main/java/com/alibaba/dashscope/aigc/multimodalconversation/MultiModalConversation.java b/src/main/java/com/alibaba/dashscope/aigc/multimodalconversation/MultiModalConversation.java index 84eebcca..c21923fb 100644 --- a/src/main/java/com/alibaba/dashscope/aigc/multimodalconversation/MultiModalConversation.java +++ b/src/main/java/com/alibaba/dashscope/aigc/multimodalconversation/MultiModalConversation.java @@ -188,7 +188,8 @@ public Flowable streamCall(MultiModalConversationP return syncApi .streamCall(param, callOption) .map(MultiModalConversationResult::fromDashScopeResult) - .map(result -> mergeSingleResponse(result, toMergeResponse, accumulatedData)); + .map(result -> mergeSingleResponse(result, toMergeResponse, accumulatedData)) + .doFinally(accumulatedData::clear); }); } From 038773ffe28c42fe5129fab0a1d32c175058599d Mon Sep 17 00:00:00 2001 From: kevin Date: Fri, 5 Jun 2026 17:40:43 +0800 Subject: [PATCH 7/8] =?UTF-8?q?fix=20Generation=E3=80=81ImageGeneration?= =?UTF-8?q?=E3=80=81MultiModelConversation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/alibaba/dashscope/aigc/generation/Generation.java | 1 - .../alibaba/dashscope/aigc/imagegeneration/ImageGeneration.java | 1 - .../aigc/multimodalconversation/MultiModalConversation.java | 1 - 3 files changed, 3 deletions(-) diff --git a/src/main/java/com/alibaba/dashscope/aigc/generation/Generation.java b/src/main/java/com/alibaba/dashscope/aigc/generation/Generation.java index b1dadadf..aca4b7e4 100644 --- a/src/main/java/com/alibaba/dashscope/aigc/generation/Generation.java +++ b/src/main/java/com/alibaba/dashscope/aigc/generation/Generation.java @@ -33,7 +33,6 @@ public final class Generation { private final SynchronizeHalfDuplexApi syncApi; private final ApiServiceOption serviceOption; - public static class Models { /** @deprecated use QWEN_TURBO instead */ @Deprecated public static final String QWEN_V1 = "qwen-v1"; diff --git a/src/main/java/com/alibaba/dashscope/aigc/imagegeneration/ImageGeneration.java b/src/main/java/com/alibaba/dashscope/aigc/imagegeneration/ImageGeneration.java index d1993ffb..0640bfdb 100644 --- a/src/main/java/com/alibaba/dashscope/aigc/imagegeneration/ImageGeneration.java +++ b/src/main/java/com/alibaba/dashscope/aigc/imagegeneration/ImageGeneration.java @@ -40,7 +40,6 @@ public final class ImageGeneration { private final ApiServiceOption serviceOption; private final String baseUrl; - public static class Models { public static final String WanX2_6_T2I = "wan2.6-t2i"; public static final String WanX2_6_IMAGE = "wan2.6-image"; diff --git a/src/main/java/com/alibaba/dashscope/aigc/multimodalconversation/MultiModalConversation.java b/src/main/java/com/alibaba/dashscope/aigc/multimodalconversation/MultiModalConversation.java index c21923fb..2403e283 100644 --- a/src/main/java/com/alibaba/dashscope/aigc/multimodalconversation/MultiModalConversation.java +++ b/src/main/java/com/alibaba/dashscope/aigc/multimodalconversation/MultiModalConversation.java @@ -38,7 +38,6 @@ public final class MultiModalConversation { private final SynchronizeHalfDuplexApi syncApi; private final ApiServiceOption serviceOption; - public static class Models { public static final String QWEN_VL_CHAT_V1 = "qwen-vl-chat-v1"; public static final String QWEN_VL_PLUS = "qwen-vl-plus"; From 59dff7d18d20f20b2feffb57c07e06d88e3b8b30 Mon Sep 17 00:00:00 2001 From: kevin Date: Fri, 5 Jun 2026 18:05:56 +0800 Subject: [PATCH 8/8] =?UTF-8?q?fix=20SteamingMerger=E3=80=81TingWu?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dashscope/multimodal/tingwu/TingWu.java | 25 ++++++++- .../dashscope/utils/StreamingMerger.java | 55 ++++++++++--------- 2 files changed, 51 insertions(+), 29 deletions(-) diff --git a/src/main/java/com/alibaba/dashscope/multimodal/tingwu/TingWu.java b/src/main/java/com/alibaba/dashscope/multimodal/tingwu/TingWu.java index b8e7a214..c8c75bcd 100644 --- a/src/main/java/com/alibaba/dashscope/multimodal/tingwu/TingWu.java +++ b/src/main/java/com/alibaba/dashscope/multimodal/tingwu/TingWu.java @@ -29,6 +29,22 @@ private ApiServiceOption defaultApiServiceOption() { .build(); } + /** Creates a copy of the shared serviceOption for thread-safe per-call usage. */ + private ApiServiceOption copyServiceOption() { + return ApiServiceOption.builder() + .protocol(serviceOption.getProtocol()) + .httpMethod(serviceOption.getHttpMethod()) + .streamingMode(serviceOption.getStreamingMode()) + .outputMode(serviceOption.getOutputMode()) + .taskGroup(serviceOption.getTaskGroup()) + .task(serviceOption.getTask()) + .function(serviceOption.getFunction()) + .isService(false) + .baseHttpUrl(serviceOption.getBaseHttpUrl()) + .baseWebSocketUrl(serviceOption.getBaseWebSocketUrl()) + .build(); + } + public TingWu() { serviceOption = defaultApiServiceOption(); syncApi = new SynchronizeHalfDuplexApi<>(serviceOption); @@ -66,8 +82,9 @@ public TingWu(String protocol, String baseUrl, ConnectionOptions connectionOptio public DashScopeResult call(HalfDuplexServiceParam param) throws ApiException, NoApiKeyException, InputRequiredException { param.validate(); - serviceOption.setIsSSE(false); - return syncApi.call(param); + ApiServiceOption callOption = copyServiceOption(); + callOption.setIsSSE(false); + return syncApi.call(param, callOption); } /** @@ -82,6 +99,8 @@ public DashScopeResult call(HalfDuplexServiceParam param) public void call(HalfDuplexServiceParam param, ResultCallback callback) throws ApiException, NoApiKeyException, InputRequiredException { param.validate(); - syncApi.call(param, callback); + ApiServiceOption callOption = copyServiceOption(); + callOption.setIsSSE(false); + syncApi.call(param, callback, callOption); } } diff --git a/src/main/java/com/alibaba/dashscope/utils/StreamingMerger.java b/src/main/java/com/alibaba/dashscope/utils/StreamingMerger.java index fc3d3be8..f4f87226 100644 --- a/src/main/java/com/alibaba/dashscope/utils/StreamingMerger.java +++ b/src/main/java/com/alibaba/dashscope/utils/StreamingMerger.java @@ -11,37 +11,40 @@ public final class StreamingMerger { private StreamingMerger() {} - public static void clearAccumulatedData(ThreadLocal> accumulatedDataMap) { - accumulatedDataMap.get().clear(); - accumulatedDataMap.remove(); - } - public static void mergeTextContent( List> currentContent, List> accumulatedContent) { + if (currentContent == null || accumulatedContent == null) { + return; + } + for (Map contentItem : currentContent) { - if (contentItem.containsKey("text")) { - String textValue = (String) contentItem.get("text"); - if (textValue != null && !textValue.isEmpty()) { - Map accumulatedTextItem = null; - for (Map accItem : accumulatedContent) { - if (accItem.containsKey("text")) { - accumulatedTextItem = accItem; - break; - } - } + if (contentItem == null || !contentItem.containsKey("text")) { + continue; + } - if (accumulatedTextItem == null) { - accumulatedTextItem = new HashMap<>(); - accumulatedTextItem.put("text", textValue); - accumulatedContent.add(accumulatedTextItem); - } else { - String existingText = (String) accumulatedTextItem.get("text"); - if (existingText == null) { - existingText = ""; - } - accumulatedTextItem.put("text", existingText + textValue); - } + String textValue = (String) contentItem.get("text"); + if (textValue == null || textValue.isEmpty()) { + continue; + } + + Map accumulatedTextItem = null; + for (Map accumulatedItem : accumulatedContent) { + if (accumulatedItem != null && accumulatedItem.containsKey("text")) { + accumulatedTextItem = accumulatedItem; + break; + } + } + + if (accumulatedTextItem == null) { + accumulatedTextItem = new HashMap<>(); + accumulatedTextItem.put("text", textValue); + accumulatedContent.add(accumulatedTextItem); + } else { + String existingText = (String) accumulatedTextItem.get("text"); + if (existingText == null) { + existingText = ""; } + accumulatedTextItem.put("text", existingText + textValue); } } }