From 4415c7a687ddd2016c9e78d76c7416dbb3813b53 Mon Sep 17 00:00:00 2001 From: foleydang Date: Fri, 23 Oct 2026 18:24:19 +0800 Subject: [PATCH 1/2] feat(agentstudio): carry inference parameters in the agent model config The agent `model` field is a config object, not only a model id. ModelConfig gains the four optional inference parameters of the Managed Agent API (temperature, max_tokens, enable_thinking, thinking_budget) plus builder(), toBuilder() and of(id); the param classes send that object as-is instead of rebuilding {"id": ...}. Shaped after the Claude Managed Agents client, where beta.agents AgentCreateParams exposes model(...) overloads for a bare model id and for a ModelConfigParams object: - the field is modelConfig, so the whole config has a matching accessor pair; String getModel() still returns the id (base-class contract unchanged) - model(String) / model(ModelConfig) builder aliases, setModel(String) as an alias of setModelConfig; unset parameters are omitted so the server default applies, and an update replaces the config as a whole - create validates model.id the way update already does, instead of only failing server-side after the request has left - name is console-only: the public API neither accepts nor returns it, so the read-back case asserts it is absent - samples/AgentStudioSimple.java points at the object form, which was otherwise only discoverable from javadoc Public surface is additive: javap against the released 2.23.2 shows no removed or re-typed member. --- samples/AgentStudioSimple.java | 2 + .../dashscope/agentstudio/model/Configs.java | 32 +++ .../agentstudio/param/AgentCreateParam.java | 43 +++- .../agentstudio/param/AgentUpdateParam.java | 43 +++- .../agentstudio/resource/Agents.java | 5 + .../alibaba/dashscope/TestAgentStudio.java | 201 ++++++++++++++++++ 6 files changed, 317 insertions(+), 9 deletions(-) diff --git a/samples/AgentStudioSimple.java b/samples/AgentStudioSimple.java index 25a243b8..a7ecaa56 100644 --- a/samples/AgentStudioSimple.java +++ b/samples/AgentStudioSimple.java @@ -42,6 +42,8 @@ public static void main(String[] args) throws Exception { .create( AgentCreateParam.builder() .name("demo-agent") + // 只给模型 id;需要推理参数时改传对象: + // .model(ModelConfig.builder().id("qwen3-max").temperature(0.7).build()) .model("qwen-plus") .systemPrompt("你是一个简洁的助手。") .build()); diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/model/Configs.java b/src/main/java/com/alibaba/dashscope/agentstudio/model/Configs.java index b9218f33..b20c5d56 100644 --- a/src/main/java/com/alibaba/dashscope/agentstudio/model/Configs.java +++ b/src/main/java/com/alibaba/dashscope/agentstudio/model/Configs.java @@ -3,18 +3,50 @@ import com.google.gson.annotations.SerializedName; import java.util.List; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; public final class Configs { private Configs() {} + /** + * The {@code model} object of an agent: the model id plus its inference parameters. Unset + * parameters keep their server default; on update this object replaces the whole config. + */ @Data + @Builder(toBuilder = true) + @NoArgsConstructor + @AllArgsConstructor(access = AccessLevel.PRIVATE) public static class ModelConfig { + /** + * The id-only config. A null id yields null rather than an id-less config, so an unset model + * stays unset in the request body. + */ + public static ModelConfig of(String id) { + return id == null ? null : builder().id(id).build(); + } + @SerializedName("id") private String id; + /** Console-only: the public API neither accepts nor returns it. */ @SerializedName("name") private String name; + + @SerializedName("temperature") + private Double temperature; + + @SerializedName("max_tokens") + private Integer maxTokens; + + @SerializedName("enable_thinking") + private Boolean enableThinking; + + @SerializedName("thinking_budget") + private Integer thinkingBudget; } @Data diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/param/AgentCreateParam.java b/src/main/java/com/alibaba/dashscope/agentstudio/param/AgentCreateParam.java index ee90036b..78faad3d 100644 --- a/src/main/java/com/alibaba/dashscope/agentstudio/param/AgentCreateParam.java +++ b/src/main/java/com/alibaba/dashscope/agentstudio/param/AgentCreateParam.java @@ -2,10 +2,12 @@ package com.alibaba.dashscope.agentstudio.param; import com.alibaba.dashscope.agentstudio.model.Configs.McpServerConfig; +import com.alibaba.dashscope.agentstudio.model.Configs.ModelConfig; import com.alibaba.dashscope.agentstudio.model.Configs.MultiAgentConfig; import com.alibaba.dashscope.agentstudio.model.Configs.SkillConfig; import com.alibaba.dashscope.agentstudio.model.Configs.ToolConfig; import com.alibaba.dashscope.base.FlattenHalfDuplexParamBase; +import com.alibaba.dashscope.exception.InputRequiredException; import com.alibaba.dashscope.utils.JsonUtils; import com.google.gson.JsonObject; import java.util.List; @@ -21,7 +23,10 @@ @EqualsAndHashCode(callSuper = true) public class AgentCreateParam extends FlattenHalfDuplexParamBase { @NonNull private String name; - @NonNull private String model; + + /** Required: the model and its inference parameters. */ + @NonNull private ModelConfig modelConfig; + @Default private String description = null; @Default private String systemPrompt = null; @Default private List tools = null; @@ -30,13 +35,29 @@ public class AgentCreateParam extends FlattenHalfDuplexParamBase { @Default private MultiAgentConfig multiagent = null; @Default private Map metadata = null; + /** The model id; the full config is {@code getModelConfig()}. */ + @Override + public String getModel() { + return modelConfig.getId(); + } + + /** Alias for calling {@code setModelConfig(ModelConfig)} with {@code ModelConfig.of(modelId)}. */ + public void setModel(String modelId) { + setModelConfig(ModelConfig.of(modelId)); + } + + @Override + public void validate() throws InputRequiredException { + if (modelConfig == null || modelConfig.getId() == null || modelConfig.getId().isEmpty()) { + throw new InputRequiredException("model.id is required, e.g. .model(\"qwen3-max\")"); + } + } + @Override public JsonObject getHttpBody() { JsonObject body = new JsonObject(); body.addProperty("name", name); - JsonObject modelObj = new JsonObject(); - modelObj.addProperty("id", model); - body.add("model", modelObj); + body.add("model", JsonUtils.toJsonElement(modelConfig)); if (description != null) { body.addProperty("description", description); } @@ -61,4 +82,18 @@ public JsonObject getHttpBody() { addExtraBody(body); return body; } + + /** Adds the {@code model(...)} aliases for {@code modelConfig(...)}. */ + public abstract static class AgentCreateParamBuilder< + C extends AgentCreateParam, B extends AgentCreateParamBuilder> + extends FlattenHalfDuplexParamBase.FlattenHalfDuplexParamBaseBuilder { + /** Alias for calling {@code modelConfig(ModelConfig)} with {@code ModelConfig.of(modelId)}. */ + public B model(String modelId) { + return modelConfig(ModelConfig.of(modelId)); + } + + public B model(ModelConfig modelConfig) { + return modelConfig(modelConfig); + } + } } diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/param/AgentUpdateParam.java b/src/main/java/com/alibaba/dashscope/agentstudio/param/AgentUpdateParam.java index d9b16eb5..f409f62c 100644 --- a/src/main/java/com/alibaba/dashscope/agentstudio/param/AgentUpdateParam.java +++ b/src/main/java/com/alibaba/dashscope/agentstudio/param/AgentUpdateParam.java @@ -2,6 +2,7 @@ package com.alibaba.dashscope.agentstudio.param; import com.alibaba.dashscope.agentstudio.model.Configs.McpServerConfig; +import com.alibaba.dashscope.agentstudio.model.Configs.ModelConfig; import com.alibaba.dashscope.agentstudio.model.Configs.MultiAgentConfig; import com.alibaba.dashscope.agentstudio.model.Configs.SkillConfig; import com.alibaba.dashscope.agentstudio.model.Configs.ToolConfig; @@ -23,8 +24,11 @@ public class AgentUpdateParam extends FlattenHalfDuplexParamBase { /** Required: must equal the server's current (latest) version, or the update fails with 409. */ @Default private Integer version = null; + /** The server rejects an update that omits it (AGENT_005); re-send the current name. */ @Default private String name = null; - @Default private String model = null; + /** Omit it to leave the stored config untouched; sending one replaces it as a whole. */ + @Default private ModelConfig modelConfig = null; + @Default private String description = null; @Default private String systemPrompt = null; @Default private List tools = null; @@ -42,10 +46,8 @@ public JsonObject getHttpBody() { if (name != null) { body.addProperty("name", name); } - if (model != null) { - JsonObject modelObj = new JsonObject(); - modelObj.addProperty("id", model); - body.add("model", modelObj); + if (modelConfig != null) { + body.add("model", JsonUtils.toJsonElement(modelConfig)); } if (description != null) { body.addProperty("description", description); @@ -72,6 +74,20 @@ public JsonObject getHttpBody() { return body; } + /** The model id, or null when unset; the full config is {@code getModelConfig()}. */ + @Override + public String getModel() { + return modelConfig == null ? null : modelConfig.getId(); + } + + /** + * Alias for calling {@code setModelConfig(ModelConfig)} with {@code ModelConfig.of(modelId)}, + * which clears the parameters stored on the agent. + */ + public void setModel(String modelId) { + setModelConfig(ModelConfig.of(modelId)); + } + @Override public void validate() throws InputRequiredException { if (version == null) { @@ -79,5 +95,22 @@ public void validate() throws InputRequiredException { "version is required for update. Call agents().retrieve(agentId) first to obtain the" + " current version (server rejects updates that don't match the latest version)."); } + if (modelConfig != null && (modelConfig.getId() == null || modelConfig.getId().isEmpty())) { + throw new InputRequiredException("model.id is required, e.g. .model(\"qwen3-max\")"); + } + } + + /** Adds the {@code model(...)} aliases for {@code modelConfig(...)}. */ + public abstract static class AgentUpdateParamBuilder< + C extends AgentUpdateParam, B extends AgentUpdateParamBuilder> + extends FlattenHalfDuplexParamBase.FlattenHalfDuplexParamBaseBuilder { + /** Alias for calling {@code modelConfig(ModelConfig)} with {@code ModelConfig.of(modelId)}. */ + public B model(String modelId) { + return modelConfig(ModelConfig.of(modelId)); + } + + public B model(ModelConfig modelConfig) { + return modelConfig(modelConfig); + } } } diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/resource/Agents.java b/src/main/java/com/alibaba/dashscope/agentstudio/resource/Agents.java index 63c1a5e3..1c3988ba 100644 --- a/src/main/java/com/alibaba/dashscope/agentstudio/resource/Agents.java +++ b/src/main/java/com/alibaba/dashscope/agentstudio/resource/Agents.java @@ -75,6 +75,11 @@ public CompletableFuture createAsync(AgentCreateParam param) { if (param == null) { return AsyncHelper.failedFuture(new InputRequiredException("param is required!")); } + try { + param.validate(); + } catch (InputRequiredException e) { + return AsyncHelper.failedFuture(e); + } GeneralServiceOption opt = AgentStudioConstants.newServiceOption(HttpMethod.POST, "agents", baseUrl); return AsyncHelper.callAsync(api, AgentStudioConstants.withApiKey(apiKey, param), opt) diff --git a/src/test/java/com/alibaba/dashscope/TestAgentStudio.java b/src/test/java/com/alibaba/dashscope/TestAgentStudio.java index c3040375..9afc6c8a 100644 --- a/src/test/java/com/alibaba/dashscope/TestAgentStudio.java +++ b/src/test/java/com/alibaba/dashscope/TestAgentStudio.java @@ -3,6 +3,7 @@ import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -167,6 +168,206 @@ public void testAgentCreateMultiagentHttpBody() { assertFalse(plain.has("multiagent")); } + private static JsonObject modelBody(Configs.ModelConfig model) { + return AgentCreateParam.builder() + .name("a") + .model(model) + .build() + .getHttpBody() + .getAsJsonObject("model"); + } + + @Test + public void testAgentCreateModelCarriesInferenceParameters() { + JsonObject model = + modelBody( + Configs.ModelConfig.builder() + .id("qwen3-max") + .temperature(0.7) + .maxTokens(1024) + .enableThinking(true) + .thinkingBudget(2048) + .build()); + assertEquals("qwen3-max", model.get("id").getAsString()); + assertEquals(0.7, model.get("temperature").getAsDouble(), 1e-9); + assertEquals(1024, model.get("max_tokens").getAsInt()); + assertTrue(model.get("enable_thinking").getAsBoolean()); + assertEquals(2048, model.get("thinking_budget").getAsInt()); + // Field order follows the API reference; unset parameters are omitted, not sent as null. + assertEquals( + "{\"id\":\"qwen3-max\",\"temperature\":0.7,\"max_tokens\":1024," + + "\"enable_thinking\":true,\"thinking_budget\":2048}", + JsonUtils.toJson(model)); + } + + @Test + public void testAgentCreateModelShorthandOmitsParameters() { + JsonObject model = + AgentCreateParam.builder() + .name("a") + .model("qwen-max") + .build() + .getHttpBody() + .getAsJsonObject("model"); + assertEquals("qwen-max", model.get("id").getAsString()); + assertFalse(model.has("temperature")); + assertFalse(model.has("max_tokens")); + assertFalse(model.has("enable_thinking")); + assertFalse(model.has("thinking_budget")); + } + + @Test + public void testAgentCreateModelKeepsFalsyParameters() { + JsonObject model = + modelBody( + Configs.ModelConfig.builder() + .id("qwen3-max") + .temperature(0.0) + .enableThinking(false) + .build()); + assertEquals(0.0, model.get("temperature").getAsDouble(), 1e-9); + assertFalse(model.get("enable_thinking").getAsBoolean()); + } + + @Test + public void testAgentUpdateModelOverwritesAndOmits() { + JsonObject withModel = + AgentUpdateParam.builder() + .version(4) + .model(Configs.ModelConfig.builder().id("qwen-plus").temperature(0.2).build()) + .build() + .getHttpBody(); + assertEquals(0.2, withModel.getAsJsonObject("model").get("temperature").getAsDouble(), 1e-9); + // Only what the object holds is sent; the server replaces the whole config with it. + assertFalse(withModel.getAsJsonObject("model").has("max_tokens")); + assertFalse( + AgentUpdateParam.builder().version(4).name("renamed").build().getHttpBody().has("model")); + } + + @Test + public void testAgentModelConfigRoundTripsAndCarriesSiblings() { + // The public API returns the id plus the four parameters; name is console-only. + Agent agent = + JsonUtils.fromJson( + "{\"id\":\"agent_1\",\"model\":{\"id\":\"qwen3-max\"," + + "\"temperature\":0.3,\"max_tokens\":2048,\"enable_thinking\":true," + + "\"thinking_budget\":1024}}", + Agent.class); + Configs.ModelConfig read = agent.getModel(); + assertEquals("qwen3-max", read.getId()); + assertNull(read.getName()); + assertEquals(0.3, read.getTemperature(), 1e-9); + assertEquals(Integer.valueOf(2048), read.getMaxTokens()); + assertTrue(read.getEnableThinking()); + assertEquals(Integer.valueOf(1024), read.getThinkingBudget()); + + Configs.ModelConfig tuned = read.toBuilder().temperature(0.9).build(); + assertEquals("qwen3-max", tuned.getId()); + assertEquals(Integer.valueOf(1024), tuned.getThinkingBudget()); + assertEquals(0.9, tuned.getTemperature(), 1e-9); + } + + @Test + public void testAgentParamGetModelReturnsTheId() { + assertEquals( + "qwen-max", AgentCreateParam.builder().name("a").model("qwen-max").build().getModel()); + assertEquals( + "qwen-max", + AgentUpdateParam.builder() + .version(1) + .model(Configs.ModelConfig.of("qwen-max")) + .build() + .getModel()); + assertNull(AgentUpdateParam.builder().version(1).build().getModel()); + } + + @Test + public void testAgentParamEqualsAndHashCodeCoverTheWholeModelConfig() { + AgentUpdateParam temperature = + AgentUpdateParam.builder() + .version(1) + .model(Configs.ModelConfig.builder().id("qwen-max").temperature(0.7).build()) + .build(); + AgentUpdateParam same = + AgentUpdateParam.builder() + .version(1) + .model(Configs.ModelConfig.builder().id("qwen-max").temperature(0.7).build()) + .build(); + AgentUpdateParam other = + AgentUpdateParam.builder() + .version(1) + .model(Configs.ModelConfig.builder().id("qwen-max").temperature(0.1).build()) + .build(); + assertEquals(temperature, same); + assertEquals(temperature.hashCode(), same.hashCode()); + assertNotEquals(temperature, other); + assertTrue(temperature.toString().contains("temperature=0.7"), temperature.toString()); + assertFalse(other.toString().contains("temperature=0.7"), other.toString()); + } + + @Test + public void testAgentModelStringNullStaysUnset() { + // null keeps the "don't touch the model" meaning instead of an empty config object. + AgentUpdateParam update = AgentUpdateParam.builder().version(1).model((String) null).build(); + assertFalse(update.getHttpBody().has("model")); + assertNull(update.getModel()); + assertThrows( + NullPointerException.class, + () -> AgentCreateParam.builder().name("a").model((String) null).build()); + } + + @Test + public void testAgentParamSetModelAcceptsIdAndConfig() { + AgentUpdateParam update = AgentUpdateParam.builder().version(1).build(); + update.setModel("qwen-max"); + assertEquals("qwen-max", update.getModel()); + assertFalse(update.getHttpBody().getAsJsonObject("model").has("temperature")); + update.setModelConfig(Configs.ModelConfig.builder().id("qwen3-max").temperature(0.7).build()); + assertEquals("qwen3-max", update.getModel()); + assertEquals("qwen3-max", update.getModelConfig().getId()); + assertEquals( + 0.7, update.getHttpBody().getAsJsonObject("model").get("temperature").getAsDouble(), 1e-9); + update.setModel(null); + assertFalse(update.getHttpBody().has("model")); + + AgentCreateParam create = AgentCreateParam.builder().name("a").model("qwen-max").build(); + create.setModelConfig(Configs.ModelConfig.of("qwen3-max")); + assertEquals("qwen3-max", create.getModel()); + assertThrows(NullPointerException.class, () -> create.setModel(null)); + assertThrows(NullPointerException.class, () -> create.setModelConfig(null)); + } + + @Test + public void testAgentCreateRejectsModelWithoutId() { + ApiException e = + assertThrows( + ApiException.class, + () -> + new Agents(null, null, null) + .create( + AgentCreateParam.builder() + .name("a") + .model(Configs.ModelConfig.builder().temperature(0.5).build()) + .build())); + assertTrue(e.getMessage().contains("model.id"), e.getMessage()); + } + + @Test + public void testAgentUpdateRejectsModelWithoutId() { + ApiException e = + assertThrows( + ApiException.class, + () -> + new Agents(null, null, null) + .update( + "agent_xyz", + AgentUpdateParam.builder() + .version(1) + .model(Configs.ModelConfig.builder().id("").build()) + .build())); + assertTrue(e.getMessage().contains("model.id"), e.getMessage()); + } + @Test public void testThreadEventExposesThreadId() { String json = From 7725e787c527a3d35c9a13e79c2fd3e7342d6e54 Mon Sep 17 00:00:00 2001 From: foleydang Date: Fri, 23 Oct 2026 18:24:19 +0800 Subject: [PATCH 2/2] fix(agentstudio): tolerate a non-object delta payload; make the stream single-pass `delta` was typed as a JsonObject, so a frame carrying anything else made Gson throw while parsing it; the stream queued that exception and re-raised it as an ApiException, dropping every event after it. It is held as a JsonElement and read as a payload object when it is one, so such a frame is simply not a chunk instead of an aborted stream. getEventDelta() still returns a JsonObject. A second pass over one stream could never work either: the queue drains into a single reader, so the next iterator blocked for the whole timeout and then reported stream_timeout. iterator() now fails fast, the way the Python SDK's second pass raises. --- .../agentstudio/message/Message.java | 25 ++++++++--- .../resource/AgentStudioEventStream.java | 11 ++++- .../dashscope/TestAgentStudioProtocol.java | 43 +++++++++++++++++++ 3 files changed, 72 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/message/Message.java b/src/main/java/com/alibaba/dashscope/agentstudio/message/Message.java index 02644583..4ffae7ff 100644 --- a/src/main/java/com/alibaba/dashscope/agentstudio/message/Message.java +++ b/src/main/java/com/alibaba/dashscope/agentstudio/message/Message.java @@ -3,6 +3,7 @@ import com.alibaba.dashscope.agentstudio.model.Session; import com.alibaba.dashscope.utils.JsonUtils; +import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.annotations.SerializedName; import java.util.HashMap; @@ -65,8 +66,10 @@ public class Message { @SerializedName("event_id") private String deltaEventId; + // Held as a JsonElement because a frame whose `delta` is not an object would otherwise fail + // deserialization, and one failed frame ends the whole stream. @SerializedName("delta") - private JsonObject deltaPayload; + private JsonElement deltaPayload; /** * Extract stop_reason from a session_status event's data block. @@ -179,17 +182,27 @@ public JsonObject getEventStart() { return eventPayload; } + /** The {@code delta} payload object, or null when the frame carries anything else. */ + public JsonObject getDeltaPayload() { + return deltaPayload != null && deltaPayload.isJsonObject() + ? deltaPayload.getAsJsonObject() + : null; + } + + public void setDeltaPayload(JsonObject payload) { + this.deltaPayload = payload; + } + /** Delta payload from {@code event_delta} frames. */ public JsonObject getEventDelta() { - if (!"event_delta".equals(type)) return null; - return deltaPayload; + return "event_delta".equals(type) ? getDeltaPayload() : null; } /** Incremental text from an {@code event_delta}'s {@code content_delta}. */ public String getDeltaText() { - if (!"event_delta".equals(type) || deltaPayload == null) return null; - if (deltaPayload.has("content") && deltaPayload.get("content").isJsonObject()) { - JsonObject c = deltaPayload.getAsJsonObject("content"); + JsonObject delta = getEventDelta(); + if (delta != null && delta.has("content") && delta.get("content").isJsonObject()) { + JsonObject c = delta.getAsJsonObject("content"); if (c.has("text")) return c.get("text").getAsString(); } return null; diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/resource/AgentStudioEventStream.java b/src/main/java/com/alibaba/dashscope/agentstudio/resource/AgentStudioEventStream.java index a32ce75e..24651acf 100644 --- a/src/main/java/com/alibaba/dashscope/agentstudio/resource/AgentStudioEventStream.java +++ b/src/main/java/com/alibaba/dashscope/agentstudio/resource/AgentStudioEventStream.java @@ -34,6 +34,10 @@ public class AgentStudioEventStream implements Iterable, Closeable { .create(); private final BlockingQueue queue = new LinkedBlockingQueue<>(); + + /** The queue drains into one reader: a second pass would block and then time out. */ + private final AtomicBoolean passStarted = new AtomicBoolean(false); + private final AtomicBoolean closed = new AtomicBoolean(false); private final EventSource eventSource; private final long timeoutMs; @@ -126,6 +130,11 @@ private static ApiException wrapFailure(Throwable t, Response response) { @Override public Iterator iterator() { + if (!passStarted.compareAndSet(false, true)) { + throw new IllegalStateException( + "this stream is single-pass and was already consumed; open another stream to read the" + + " events again"); + } return new Iterator() { private Message next; @@ -271,7 +280,7 @@ public void close() { /** * Incremental text chunks from {@code event_delta} frames. Requires the stream to be opened with * {@code event_deltas}; otherwise yields nothing (use {@link TextStream} for terminal full text). - * Stops on terminal {@code session_status}. + * Stops on terminal {@code session_status}. Consumes the stream's single pass. */ public static class TextDeltaStream implements Iterable, Closeable { private final AgentStudioEventStream source; diff --git a/src/test/java/com/alibaba/dashscope/TestAgentStudioProtocol.java b/src/test/java/com/alibaba/dashscope/TestAgentStudioProtocol.java index 21c24181..2f9f60ef 100644 --- a/src/test/java/com/alibaba/dashscope/TestAgentStudioProtocol.java +++ b/src/test/java/com/alibaba/dashscope/TestAgentStudioProtocol.java @@ -14,10 +14,16 @@ import com.alibaba.dashscope.agentstudio.model.Session; import com.alibaba.dashscope.agentstudio.param.AgentCreateParam; import com.alibaba.dashscope.agentstudio.param.SessionEventListParam; +import com.alibaba.dashscope.agentstudio.resource.AgentStudioEventStream; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonObject; +import java.io.IOException; import java.util.Arrays; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; import org.junit.jupiter.api.Test; /** @@ -95,6 +101,43 @@ void testMessageDeltaText() { assertEquals("hello", msg.getDeltaText()); } + @Test + void testMessageWithNonObjectDeltaIsStillParsed() { + // A frame Gson cannot fit used to abort the whole stream; now it is simply not a chunk. + Message msg = + GSON.fromJson( + "{\"object\":\"content\",\"status\":\"in_progress\",\"type\":\"text\"," + + "\"index\":0,\"delta\":true,\"text\":\"hi\"}", + Message.class); + assertEquals("content", msg.getObject()); + assertNull(msg.getEventDelta()); + assertNull(msg.getDeltaText()); + } + + @Test + void testStreamIsSinglePass() throws IOException { + MockWebServer server = new MockWebServer(); + server.enqueue( + new MockResponse() + .setHeader("Content-Type", "text/event-stream") + .setBody("data: {\"type\":\"session_status\"}\n\n")); + server.start(); + OkHttpClient client = new OkHttpClient(); + try (AgentStudioEventStream stream = + new AgentStudioEventStream( + client, new Request.Builder().url(server.url("/events")).build(), 5000L)) { + int seen = 0; + for (Message ignored : stream) { + seen++; + } + assertEquals(1, seen); + assertThrows(IllegalStateException.class, stream::iterator); + } finally { + client.dispatcher().executorService().shutdown(); + server.shutdown(); + } + } + @Test void testMessageEventStart() { String json = "{\"type\":\"event_start\",\"event\":{\"id\":\"sevt_a\",\"type\":\"message\"}}";