currentToolCalls = choice.getMessage().getToolCalls();
if (currentToolCalls != null && !currentToolCalls.isEmpty()) {
mergeToolCalls(currentToolCalls, accumulated.toolCalls);
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..2825c89a 100644
--- a/src/main/java/com/alibaba/dashscope/aigc/multimodalconversation/MultiModalConversationParam.java
+++ b/src/main/java/com/alibaba/dashscope/aigc/multimodalconversation/MultiModalConversationParam.java
@@ -91,19 +91,24 @@ public class MultiModalConversationParam extends HalfDuplexServiceParam {
/**
* Used to control the streaming output mode. If true, the subsequent output will include the
* previously input content by default. Otherwise, the subsequent output will not include the
- * previously output content. Default: false eg(false):
+ * previously output content. Default: false eg(false): /** Used to control the streaming output
+ * mode. If true, each chunk contains only incremental content without accumulation. If false, SDK
+ * accumulates chunks locally and returns full content in each chunk. Must be explicitly set - no
+ * default value.
*
*
- * I
- * I like
- * I like apple
- * when true:
- * I
- * like
- * apple
+ * incrementalOutput=true (incremental):
+ * Chunk1: reasoning="让我先分析", content=[]
+ * Chunk2: reasoning="看起来是羊", content=[{text="这张图片"}]
+ * Chunk3: reasoning="", content=[{text="和一辆车"}]
+ *
+ * incrementalOutput=false (full):
+ * Chunk1: reasoning="让我先分析", content=[]
+ * Chunk2: reasoning="让我先分析看起来是羊", content=[{text="这张图片"}]
+ * Chunk3: reasoning="让我先分析看起来是羊", content=[{text="这张图片和一辆车"}]
*
*/
- @Builder.Default private Boolean incrementalOutput;
+ private Boolean incrementalOutput;
/** Output format of the model including "text" and "audio". Default value: ["text"] */
private List modalities;
@@ -162,6 +167,24 @@ public class MultiModalConversationParam extends HalfDuplexServiceParam {
/** thinking budget */
private Integer thinkingBudget;
+ /** stop words or token ids to stop generation */
+ @Singular("stopString")
+ private List stopStrings;
+
+ @Singular private List> stopTokens;
+
+ /**
+ * whether to return log probabilities of output tokens, supported for qwen-vl-ocr-2025-04-13 and
+ * later
+ */
+ private Boolean logprobs;
+
+ /**
+ * number of top candidate tokens to return log probabilities for, range [0,5], only effective
+ * when logprobs is true
+ */
+ private Integer topLogprobs;
+
@Override
public JsonObject getHttpBody() {
JsonObject requestObject = new JsonObject();
@@ -318,6 +341,26 @@ public Map getParameters() {
params.put("thinking_budget", thinkingBudget);
}
+ if (stopStrings != null
+ && !stopStrings.isEmpty()
+ && stopTokens != null
+ && !stopTokens.isEmpty()) {
+ throw new IllegalArgumentException("Only one of stopStrings or stopTokens can be specified.");
+ }
+ if (stopStrings != null && !stopStrings.isEmpty()) {
+ params.put(ApiKeywords.STOP, stopStrings);
+ } else if (stopTokens != null && !stopTokens.isEmpty()) {
+ params.put(ApiKeywords.STOP, stopTokens);
+ }
+
+ if (logprobs != null) {
+ params.put("logprobs", logprobs);
+ }
+
+ if (topLogprobs != null) {
+ params.put("top_logprobs", topLogprobs);
+ }
+
params.putAll(parameters);
return params;
}
diff --git a/src/main/java/com/alibaba/dashscope/common/MultiModalMessageAdapter.java b/src/main/java/com/alibaba/dashscope/common/MultiModalMessageAdapter.java
index 9327ea6c..6717f643 100644
--- a/src/main/java/com/alibaba/dashscope/common/MultiModalMessageAdapter.java
+++ b/src/main/java/com/alibaba/dashscope/common/MultiModalMessageAdapter.java
@@ -13,9 +13,12 @@
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import java.io.IOException;
+import java.math.BigDecimal;
+import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
+import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -37,14 +40,18 @@ private void writeValue(JsonWriter out, Object value) throws IOException {
out.nullValue();
} else if (value instanceof String) {
out.value((String) value);
- } else if (value instanceof Integer) {
- out.value((Integer) value);
- } else if (value instanceof Long) {
- out.value((Long) value);
- } else if (value instanceof Double) {
- out.value((Double) value);
- } else if (value instanceof Float) {
- out.value((Float) value);
+ } else if (value instanceof Integer || value instanceof Long) {
+ out.value(((Number) value).longValue());
+ } else if (value instanceof Float || value instanceof Double) {
+ out.value(((Number) value).doubleValue());
+ } else if (value instanceof Short || value instanceof Byte) {
+ out.value(((Number) value).intValue());
+ } else if (value instanceof BigDecimal) {
+ // Use jsonValue to write as JSON number, not string
+ out.jsonValue(((BigDecimal) value).toPlainString());
+ } else if (value instanceof BigInteger) {
+ // Use jsonValue to write as JSON number, not string
+ out.jsonValue(((BigInteger) value).toString());
} else if (value instanceof Boolean) {
out.value((Boolean) value);
} else if (value instanceof Character) {
@@ -59,8 +66,9 @@ private void writeValue(JsonWriter out, Object value) throws IOException {
} else if (value instanceof Map) {
writeMapObject(out, (Map) value);
} else {
- // Fallback for other types
- out.value(value.toString());
+ // For unsupported types, serialize using Gson to JSON string
+ String jsonStr = JsonUtils.toJson(value);
+ out.jsonValue(jsonStr);
}
}
@@ -95,8 +103,8 @@ private void writeToolCallBase(JsonWriter writer, ToolCallBase toolCallBase) thr
}
writer.endObject();
} else if (toolCallBase instanceof ToolCallCodeInterpreter) {
- // For ToolCallCodeInterpreter no extra fields besides id and type
- // Any additional fields specific to this should be written here.
+ // ToolCallCodeInterpreter only has id, type, and index fields
+ // id and type are already written above, index is handled in common fields
}
writer.endObject();
@@ -113,10 +121,13 @@ private ToolCallFunction convertToCallFunction(LinkedTreeMap too
callFunction.setName(fc.get("name").toString());
}
if (fc.containsKey("arguments")) {
- callFunction.setArguments(fc.get("arguments").toString());
+ Object args = fc.get("arguments");
+ callFunction.setArguments(args instanceof String ? (String) args : JsonUtils.toJson(args));
}
if (fc.containsKey("output")) {
- callFunction.setOutput(fc.get("output").toString());
+ Object output = fc.get("output");
+ callFunction.setOutput(
+ output instanceof String ? (String) output : JsonUtils.toJson(output));
}
functionCall.setFunction(callFunction);
}
@@ -133,18 +144,89 @@ private ToolCallFunction convertToCallFunction(LinkedTreeMap too
return functionCall;
}
+ // Convert LinkedTreeMap to ToolCallQuarkSearch
+ @SuppressWarnings("unchecked")
+ private ToolCallQuarkSearch convertToQuarkSearch(LinkedTreeMap toolCall) {
+ ToolCallQuarkSearch quarkSearch = new ToolCallQuarkSearch();
+ if (toolCall.containsKey("quark_search")) {
+ LinkedTreeMap qs =
+ (LinkedTreeMap) toolCall.get("quark_search");
+ Map searchParams = new HashMap<>();
+ for (Map.Entry entry : qs.entrySet()) {
+ Object val = entry.getValue();
+ if (val instanceof String) {
+ searchParams.put(entry.getKey(), (String) val);
+ } else {
+ // For non-string types, serialize to JSON string
+ searchParams.put(entry.getKey(), JsonUtils.toJson(val));
+ }
+ }
+ quarkSearch.setQuarkSearch(searchParams);
+ }
+ quarkSearch.setType(toolCall.get("type").toString());
+ if (toolCall.containsKey("id")) {
+ quarkSearch.setId(toolCall.get("id").toString());
+ }
+ if (toolCall.containsKey("index")) {
+ Object indexObj = toolCall.get("index");
+ if (indexObj instanceof Number) {
+ quarkSearch.setIndex(((Number) indexObj).intValue());
+ }
+ }
+ return quarkSearch;
+ }
+
+ // Convert LinkedTreeMap to ToolCallCodeInterpreter
+ @SuppressWarnings("unchecked")
+ private ToolCallCodeInterpreter convertToCodeInterpreter(LinkedTreeMap toolCall) {
+ ToolCallCodeInterpreter codeInterpreter = new ToolCallCodeInterpreter();
+ codeInterpreter.setType(toolCall.get("type").toString());
+ if (toolCall.containsKey("id")) {
+ codeInterpreter.setId(toolCall.get("id").toString());
+ }
+ if (toolCall.containsKey("index")) {
+ Object indexObj = toolCall.get("index");
+ if (indexObj instanceof Number) {
+ codeInterpreter.setIndex(((Number) indexObj).intValue());
+ }
+ }
+ return codeInterpreter;
+ }
+
+ // Generic method to convert LinkedTreeMap to appropriate ToolCallBase subclass
+ @SuppressWarnings("unchecked")
+ private ToolCallBase convertToToolCall(LinkedTreeMap toolCall) {
+ if (!toolCall.containsKey("type")) {
+ throw new IllegalArgumentException("Tool call must contain 'type' field");
+ }
+
+ String type = toolCall.get("type").toString();
+ switch (type) {
+ case "function":
+ return convertToCallFunction(toolCall);
+ case "quark_search":
+ return convertToQuarkSearch(toolCall);
+ case "code_interpreter":
+ return convertToCodeInterpreter(toolCall);
+ default:
+ throw new IllegalArgumentException("Unknown tool call type: " + type);
+ }
+ }
+
@Override
public void write(JsonWriter out, MultiModalMessage value) throws IOException {
out.beginObject();
out.name(ApiKeywords.ROLE);
out.value(value.getRole());
- out.name(ApiKeywords.CONTENT);
- out.beginArray();
- for (Map item : value.getContent()) {
- writeMapObject(out, item);
+ if (value.getContent() != null) {
+ out.name(ApiKeywords.CONTENT);
+ out.beginArray();
+ for (Map item : value.getContent()) {
+ writeMapObject(out, item);
+ }
+ out.endArray();
}
- out.endArray();
if (value.getAnnotations() != null) {
out.name(ApiKeywords.ANNOTATIONS);
@@ -164,8 +246,7 @@ public void write(JsonWriter out, MultiModalMessage value) throws IOException {
out.name(ApiKeywords.TOOL_CALLS);
out.beginArray();
List toolCalls = value.getToolCalls();
- for (ToolCallBase tc :
- JsonUtils.fromJson(JsonUtils.toJson(toolCalls), ToolCallBase[].class)) {
+ for (ToolCallBase tc : toolCalls) {
writeToolCallBase(out, tc);
}
out.endArray();
@@ -199,20 +280,37 @@ public MultiModalMessage read(JsonReader in) throws IOException {
Object content = objectMap.get(ApiKeywords.CONTENT);
if (content instanceof String) {
msg.setContent(Arrays.asList(Collections.singletonMap("text", (String) content)));
- } else {
+ } else if (content instanceof List) {
msg.setContent((List