Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
<name>DashScope Java SDK</name>
<groupId>com.alibaba</groupId>
<artifactId>dashscope-sdk-java</artifactId>
<version>2.22.23</version>
<version>2.22.24</version>

<properties>
<maven.compiler.source>8</maven.compiler.source>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import com.alibaba.dashscope.common.Function;
import com.alibaba.dashscope.common.OutputMode;
import com.alibaba.dashscope.common.ResultCallback;
import com.alibaba.dashscope.common.SearchInfo;
import com.alibaba.dashscope.common.Task;
import com.alibaba.dashscope.common.TaskGroup;
import com.alibaba.dashscope.exception.ApiException;
Expand Down Expand Up @@ -315,6 +316,19 @@ private GenerationResult mergeSingleResponse(
return null;
}

// Handle searchInfo accumulation at output level
if (result.getOutput().getSearchInfo() != null) {
// Store in the first accumulated data entry
AccumulatedData firstAccumulated =
accumulatedData.computeIfAbsent(0, k -> new AccumulatedData());
firstAccumulated.searchInfo = result.getOutput().getSearchInfo();
}
// Always restore accumulated searchInfo to output
AccumulatedData firstAccumulatedData = accumulatedData.get(0);
if (firstAccumulatedData != null && firstAccumulatedData.searchInfo != null) {
result.getOutput().setSearchInfo(firstAccumulatedData.searchInfo);
}

for (GenerationOutput.Choice choice : choices) {
// Use the choice's index field for accumulation, fallback to 0
Integer choiceIndex = choice.getIndex();
Expand Down Expand Up @@ -481,6 +495,11 @@ private GenerationResult mergeSingleResponse(
}
}
output.setChoices(allChoices);
// Restore searchInfo at output level
AccumulatedData firstData = accumulatedData.get(0);
if (firstData != null && firstData.searchInfo != null) {
output.setSearchInfo(firstData.searchInfo);
}
Comment on lines 497 to +502

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This block is redundant because searchInfo is already restored at the output level on lines 326-330 of mergeSingleResponse(). Since output is a reference to result.getOutput(), any changes made to it are already reflected, and there is no need to restore it again here.

Suggested change
output.setChoices(allChoices);
// Restore searchInfo at output level
AccumulatedData firstData = accumulatedData.get(0);
if (firstData != null && firstData.searchInfo != null) {
output.setSearchInfo(firstData.searchInfo);
}
output.setChoices(allChoices);

if (result.getUsage() != null && totalOutputTokens > 0) {
result.getUsage().setOutputTokens(totalOutputTokens);
if (result.getUsage().getInputTokens() != null) {
Expand Down Expand Up @@ -529,6 +548,13 @@ private GenerationResult mergeSingleResponse(
if (accumulated.content.length() > 0) {
result.getOutput().setText(accumulated.content.toString());
}
// Handle searchInfo accumulation for legacy format
if (result.getOutput().getSearchInfo() != null) {
accumulated.searchInfo = result.getOutput().getSearchInfo();
}
if (accumulated.searchInfo != null) {
result.getOutput().setSearchInfo(accumulated.searchInfo);
}
}

return result;
Expand Down Expand Up @@ -649,5 +675,6 @@ private static class AccumulatedData {
boolean allChoicesSent = false;
String role = null;
Integer outputTokens = null;
SearchInfo searchInfo = null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
// Copyright (c) Alibaba, Inc. and its affiliates.

package com.alibaba.dashscope;

import static org.junit.jupiter.api.Assertions.*;

import com.alibaba.dashscope.aigc.generation.Generation;
import com.alibaba.dashscope.aigc.generation.GenerationParam;
import com.alibaba.dashscope.aigc.generation.GenerationResult;
import com.alibaba.dashscope.exception.ApiException;
import com.alibaba.dashscope.exception.InputRequiredException;
import com.alibaba.dashscope.exception.NoApiKeyException;
import com.alibaba.dashscope.utils.Constants;
import io.reactivex.Flowable;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.parallel.Execution;
import org.junit.jupiter.api.parallel.ExecutionMode;
import org.junitpioneer.jupiter.SetEnvironmentVariable;

@Execution(ExecutionMode.SAME_THREAD)
@SetEnvironmentVariable(key = "DASHSCOPE_API_KEY", value = "1234")
public class TestGenerationStreamSearchInfo {
private static MockWebServer mockServer;

// Choices format messages - searchInfo appears at output level in first chunk
private String choicesMsg1WithSearchInfo =
"{\"output\":{\"choices\":[{\"message\":{\"content\":\"今天\",\"role\":\"assistant\"},\"finish_reason\":\"null\"}],\"search_info\":{\"search_results\":[{\"site_name\":\"weather.com\",\"icon\":\"\",\"index\":1,\"title\":\"Weather\",\"url\":\"https://weather.com\"}]}},\"usage\":{\"total_tokens\":27,\"input_tokens\":26,\"output_tokens\":1},\"request_id\":\"test-req-1\"}";
private String choicesMsg2NoSearchInfo =
"{\"output\":{\"choices\":[{\"message\":{\"content\":\"晴天\",\"role\":\"assistant\"},\"finish_reason\":\"stop\"}]},\"usage\":{\"total_tokens\":28,\"input_tokens\":26,\"output_tokens\":2},\"request_id\":\"test-req-1\"}";

// Choices format messages - searchInfo appears at output level in middle chunk
private String choicesMsg3NoSearchInfo =
"{\"output\":{\"choices\":[{\"message\":{\"content\":\"明天\",\"role\":\"assistant\"},\"finish_reason\":\"null\"}]},\"usage\":{\"total_tokens\":27,\"input_tokens\":26,\"output_tokens\":1},\"request_id\":\"test-req-2\"}";
private String choicesMsg4WithSearchInfo =
"{\"output\":{\"choices\":[{\"message\":{\"content\":\"多云\",\"role\":\"assistant\"},\"finish_reason\":\"null\"}],\"search_info\":{\"search_results\":[{\"site_name\":\"weather2.com\",\"icon\":\"\",\"index\":1,\"title\":\"Weather2\",\"url\":\"https://weather2.com\"}]}},\"usage\":{\"total_tokens\":28,\"input_tokens\":26,\"output_tokens\":2},\"request_id\":\"test-req-2\"}";
private String choicesMsg5NoSearchInfo =
"{\"output\":{\"choices\":[{\"message\":{\"content\":\"转晴\",\"role\":\"assistant\"},\"finish_reason\":\"stop\"}]},\"usage\":{\"total_tokens\":29,\"input_tokens\":26,\"output_tokens\":3},\"request_id\":\"test-req-2\"}";

// Legacy text format messages - searchInfo appears in first chunk
private String legacyMsg1WithSearchInfo =
"{\"output\":{\"text\":\"今天\",\"search_info\":{\"search_results\":[{\"site_name\":\"weather.com\",\"icon\":\"\",\"index\":1,\"title\":\"Weather\",\"url\":\"https://weather.com\"}]}},\"usage\":{\"total_tokens\":27,\"input_tokens\":26,\"output_tokens\":1},\"request_id\":\"test-req-3\"}";
private String legacyMsg2NoSearchInfo =
"{\"output\":{\"text\":\"晴天\"},\"usage\":{\"total_tokens\":28,\"input_tokens\":26,\"output_tokens\":2},\"request_id\":\"test-req-3\"}";

// Legacy text format messages - searchInfo appears in second chunk
private String legacyMsg3NoSearchInfo =
"{\"output\":{\"text\":\"明天\"},\"usage\":{\"total_tokens\":27,\"input_tokens\":26,\"output_tokens\":1},\"request_id\":\"test-req-4\"}";
private String legacyMsg4WithSearchInfo =
"{\"output\":{\"text\":\"多云\",\"search_info\":{\"search_results\":[{\"site_name\":\"weather.com\",\"icon\":\"\",\"index\":1,\"title\":\"Weather\",\"url\":\"https://weather.com\"}]}},\"usage\":{\"total_tokens\":28,\"input_tokens\":26,\"output_tokens\":2},\"request_id\":\"test-req-4\"}";

@BeforeAll
public static void before() throws IOException {
mockServer = new MockWebServer();
mockServer.start();
}

@AfterAll
public static void after() throws IOException {
mockServer.close();
}

@Test
public void testSearchInfoPreservedInChoicesFormat_FirstChunk()
throws ApiException, NoApiKeyException, IOException, InterruptedException,
InputRequiredException {
// SearchInfo appears in first chunk, should be preserved in all subsequent chunks
MockResponse mockResponse =
TestUtils.createStreamMockResponse(
Arrays.asList(choicesMsg1WithSearchInfo, choicesMsg2NoSearchInfo), 200);
mockServer.enqueue(mockResponse);

GenerationParam param =
GenerationParam.builder()
.model(Generation.Models.QWEN_TURBO)
.prompt("test")
.resultFormat(GenerationParam.ResultFormat.MESSAGE)
.incrementalOutput(false)
.build();
Constants.baseHttpApiUrl = String.format("http://127.0.0.1:%s", mockServer.getPort());

Generation generation = new Generation();
Flowable<GenerationResult> flowable = generation.streamCall(param);
List<GenerationResult> results = new ArrayList<>();
flowable.blockingForEach(results::add);

assertEquals(2, results.size());

// Debug: print results
System.out.println("=== Choices First Chunk Test ===");
for (int i = 0; i < results.size(); i++) {
System.out.println(
"Result " + i + ": " + com.alibaba.dashscope.utils.JsonUtils.toJson(results.get(i)));
System.out.println(" searchInfo: " + results.get(i).getOutput().getSearchInfo());
}

// First chunk should have searchInfo at output level
assertNotNull(results.get(0).getOutput().getChoices());
assertNotNull(results.get(0).getOutput().getSearchInfo());
assertEquals(
"weather.com",
results.get(0).getOutput().getSearchInfo().getSearchResults().get(0).getSiteName());

// Second chunk should also have searchInfo at output level (accumulated from first chunk)
assertNotNull(results.get(1).getOutput().getChoices());
assertNotNull(results.get(1).getOutput().getSearchInfo());
assertEquals(
"weather.com",
results.get(1).getOutput().getSearchInfo().getSearchResults().get(0).getSiteName());
}

@Test
public void testSearchInfoPreservedInChoicesFormat_MiddleChunk()
throws ApiException, NoApiKeyException, IOException, InterruptedException,
InputRequiredException {
// SearchInfo appears in middle chunk, should be preserved in all chunks including later ones
MockResponse mockResponse =
TestUtils.createStreamMockResponse(
Arrays.asList(
choicesMsg3NoSearchInfo, choicesMsg4WithSearchInfo, choicesMsg5NoSearchInfo),
200);
mockServer.enqueue(mockResponse);

GenerationParam param =
GenerationParam.builder()
.model(Generation.Models.QWEN_TURBO)
.prompt("test")
.resultFormat(GenerationParam.ResultFormat.MESSAGE)
.incrementalOutput(false)
.build();
Constants.baseHttpApiUrl = String.format("http://127.0.0.1:%s", mockServer.getPort());

Generation generation = new Generation();
Flowable<GenerationResult> flowable = generation.streamCall(param);
List<GenerationResult> results = new ArrayList<>();
flowable.blockingForEach(results::add);

assertEquals(3, results.size());

// First chunk has no searchInfo at output level
assertNull(results.get(0).getOutput().getSearchInfo());

// Second chunk has searchInfo at output level
assertNotNull(results.get(1).getOutput().getSearchInfo());
assertEquals(
"weather2.com",
results.get(1).getOutput().getSearchInfo().getSearchResults().get(0).getSiteName());

// Third chunk should also have searchInfo at output level (accumulated from second chunk)
assertNotNull(results.get(2).getOutput().getSearchInfo());
assertEquals(
"weather2.com",
results.get(2).getOutput().getSearchInfo().getSearchResults().get(0).getSiteName());
}

@Test
public void testSearchInfoPreservedInLegacyFormat_FirstChunk()
throws ApiException, NoApiKeyException, IOException, InterruptedException,
InputRequiredException {
// SearchInfo appears in first chunk of legacy format, should be preserved in second chunk
MockResponse mockResponse =
TestUtils.createStreamMockResponse(
Arrays.asList(legacyMsg1WithSearchInfo, legacyMsg2NoSearchInfo), 200);
mockServer.enqueue(mockResponse);

GenerationParam param =
GenerationParam.builder()
.model(Generation.Models.QWEN_TURBO)
.prompt("test")
.resultFormat(GenerationParam.ResultFormat.TEXT)
.incrementalOutput(false)
.build();
Constants.baseHttpApiUrl = String.format("http://127.0.0.1:%s", mockServer.getPort());

Generation generation = new Generation();
Flowable<GenerationResult> flowable = generation.streamCall(param);
List<GenerationResult> results = new ArrayList<>();
flowable.blockingForEach(results::add);

assertEquals(2, results.size());

// First chunk should have searchInfo at output level
assertNotNull(results.get(0).getOutput().getSearchInfo());
assertEquals(
"weather.com",
results.get(0).getOutput().getSearchInfo().getSearchResults().get(0).getSiteName());

// Second chunk should also have searchInfo at output level (accumulated from first chunk)
assertNotNull(results.get(1).getOutput().getSearchInfo());
assertEquals(
"weather.com",
results.get(1).getOutput().getSearchInfo().getSearchResults().get(0).getSiteName());
}

@Test
public void testSearchInfoPreservedInLegacyFormat_SecondChunk()
throws ApiException, NoApiKeyException, IOException, InterruptedException,
InputRequiredException {
// SearchInfo appears in second chunk of legacy format
MockResponse mockResponse =
TestUtils.createStreamMockResponse(
Arrays.asList(legacyMsg3NoSearchInfo, legacyMsg4WithSearchInfo), 200);
mockServer.enqueue(mockResponse);

GenerationParam param =
GenerationParam.builder()
.model(Generation.Models.QWEN_TURBO)
.prompt("test")
.resultFormat(GenerationParam.ResultFormat.TEXT)
.incrementalOutput(false)
.build();
Constants.baseHttpApiUrl = String.format("http://127.0.0.1:%s", mockServer.getPort());

Generation generation = new Generation();
Flowable<GenerationResult> flowable = generation.streamCall(param);
List<GenerationResult> results = new ArrayList<>();
flowable.blockingForEach(results::add);

assertEquals(2, results.size());

// First chunk has no searchInfo at output level
assertNull(results.get(0).getOutput().getSearchInfo());

// Second chunk has searchInfo at output level
assertNotNull(results.get(1).getOutput().getSearchInfo());
assertEquals(
"weather.com",
results.get(1).getOutput().getSearchInfo().getSearchResults().get(0).getSiteName());
}
}
Loading