From 0f2b3d95f765d65a1f887fd77c933ad83f360925 Mon Sep 17 00:00:00 2001 From: "qihuai.wyq" Date: Wed, 25 Feb 2026 16:01:38 +0800 Subject: [PATCH 1/6] feat: allow manual endpoint type configuration and add qwen3.5-plus support Change-Id: I117c4adee6f8372691f9186e14b7dc26839a0528 --- .../dashscope/dto/DashScopeRequest.java | 49 +++++++++- .../core/model/DashScopeChatModel.java | 90 +++++++++++++++++- .../core/model/DashScopeHttpClient.java | 95 ++++++++++++++++--- .../agentscope/core/model/EndpointType.java | 62 ++++++++++++ 4 files changed, 278 insertions(+), 18 deletions(-) create mode 100644 agentscope-core/src/main/java/io/agentscope/core/model/EndpointType.java diff --git a/agentscope-core/src/main/java/io/agentscope/core/formatter/dashscope/dto/DashScopeRequest.java b/agentscope-core/src/main/java/io/agentscope/core/formatter/dashscope/dto/DashScopeRequest.java index 7e0ff028a..af43b1947 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/formatter/dashscope/dto/DashScopeRequest.java +++ b/agentscope-core/src/main/java/io/agentscope/core/formatter/dashscope/dto/DashScopeRequest.java @@ -15,8 +15,10 @@ */ package io.agentscope.core.formatter.dashscope.dto; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import io.agentscope.core.model.EndpointType; /** * DashScope API request DTO. @@ -53,12 +55,23 @@ public class DashScopeRequest { @JsonProperty("parameters") private DashScopeParameters parameters; - public DashScopeRequest() {} + /** + * The endpoint type for endpoint selection (not serialized to JSON). + * + *

This is an internal field used to determine which DashScope API endpoint to use. + * It does not get sent to the API. + */ + @JsonIgnore private EndpointType endpointType; + + public DashScopeRequest() { + this.endpointType = EndpointType.AUTO; + } public DashScopeRequest(String model, DashScopeInput input, DashScopeParameters parameters) { this.model = model; this.input = input; this.parameters = parameters; + this.endpointType = EndpointType.AUTO; } public String getModel() { @@ -85,6 +98,24 @@ public void setParameters(DashScopeParameters parameters) { this.parameters = parameters; } + /** + * Gets the endpoint type for endpoint selection. + * + * @return the endpoint type (defaults to AUTO) + */ + public EndpointType getEndpointType() { + return endpointType; + } + + /** + * Sets the endpoint type for endpoint selection. + * + * @param endpointType the endpoint type + */ + public void setEndpointType(EndpointType endpointType) { + this.endpointType = endpointType; + } + public static Builder builder() { return new Builder(); } @@ -93,6 +124,7 @@ public static class Builder { private String model; private DashScopeInput input; private DashScopeParameters parameters; + private EndpointType endpointType = EndpointType.AUTO; public Builder model(String model) { this.model = model; @@ -109,8 +141,21 @@ public Builder parameters(DashScopeParameters parameters) { return this; } + /** + * Sets the endpoint type for endpoint selection. + * + * @param endpointType the endpoint type + * @return this builder + */ + public Builder endpointType(EndpointType endpointType) { + this.endpointType = endpointType; + return this; + } + public DashScopeRequest build() { - return new DashScopeRequest(model, input, parameters); + DashScopeRequest request = new DashScopeRequest(model, input, parameters); + request.setEndpointType(endpointType); + return request; } } } diff --git a/agentscope-core/src/main/java/io/agentscope/core/model/DashScopeChatModel.java b/agentscope-core/src/main/java/io/agentscope/core/model/DashScopeChatModel.java index 6d381647d..5c790d7e1 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/model/DashScopeChatModel.java +++ b/agentscope-core/src/main/java/io/agentscope/core/model/DashScopeChatModel.java @@ -60,6 +60,7 @@ public class DashScopeChatModel extends ChatModelBase { private final boolean stream; private final Boolean enableThinking; // nullable private final Boolean enableSearch; // nullable + private final EndpointType endpointType; private final GenerateOptions defaultOptions; private final Formatter formatter; @@ -67,13 +68,48 @@ public class DashScopeChatModel extends ChatModelBase { private final DashScopeHttpClient httpClient; /** - * Creates a new DashScope chat model instance. + * Creates a new DashScope chat model instance with automatic API type detection. + * + *

This constructor maintains backward compatibility. API type defaults to AUTO, + * which detects the endpoint based on model name. + * + * @param apiKey the API key for DashScope authentication + * @param modelName the model name (e.g., "qwen-max", "qwen-vl-plus") + * @param stream whether streaming should be enabled (ignored if enableThinking is true) + * @param enableThinking whether thinking mode should be enabled (null for disabled) + * @param enableSearch whether search enhancement should be enabled (null for disabled) + * @param defaultOptions default generation options (null for defaults) + * @param baseUrl custom base URL for DashScope API (null for default) + * @param formatter the message formatter to use (null for default DashScope formatter) + * @param httpTransport custom HTTP transport (null for default from factory) + * @param publicKeyId the RSA public key ID for encryption (null to disable encryption) + * @param publicKey the RSA public key for encryption (Base64-encoded, null to disable encryption) + */ + public DashScopeChatModel( + String apiKey, + String modelName, + boolean stream, + Boolean enableThinking, + Boolean enableSearch, + GenerateOptions defaultOptions, + String baseUrl, + Formatter formatter, + HttpTransport httpTransport, + String publicKeyId, + String publicKey) { + this(apiKey, modelName, stream, enableThinking, enableSearch, null, + defaultOptions, baseUrl, formatter, httpTransport, publicKeyId, publicKey); + } + + /** + * Creates a new DashScope chat model instance with explicit API type. * * @param apiKey the API key for DashScope authentication * @param modelName the model name (e.g., "qwen-max", "qwen-vl-plus") * @param stream whether streaming should be enabled (ignored if enableThinking is true) * @param enableThinking whether thinking mode should be enabled (null for disabled) * @param enableSearch whether search enhancement should be enabled (null for disabled) + * @param endpointType the endpoint type to use (null for AUTO detection) * @param defaultOptions default generation options (null for defaults) * @param baseUrl custom base URL for DashScope API (null for default) * @param formatter the message formatter to use (null for default DashScope formatter) @@ -87,6 +123,7 @@ public DashScopeChatModel( boolean stream, Boolean enableThinking, Boolean enableSearch, + EndpointType endpointType, GenerateOptions defaultOptions, String baseUrl, Formatter formatter, @@ -103,6 +140,7 @@ public DashScopeChatModel( this.stream = enableThinking != null && enableThinking ? true : stream; this.enableThinking = enableThinking; this.enableSearch = enableSearch; + this.endpointType = endpointType != null ? endpointType : EndpointType.AUTO; this.defaultOptions = defaultOptions != null ? defaultOptions : GenerateOptions.builder().build(); this.formatter = formatter != null ? formatter : new DashScopeChatFormatter(); @@ -150,8 +188,12 @@ protected Flux doStream( List messages, List tools, GenerateOptions options) { if (log.isDebugEnabled()) { - boolean useMultimodal = httpClient.requiresMultimodalApi(modelName); - log.debug("DashScope API call: model={}, multimodal={}", modelName, useMultimodal); + boolean useMultimodal = httpClient.requiresMultimodalApi(modelName, endpointType); + log.debug( + "DashScope API call: model={}, endpointType={}, multimodal={}", + modelName, + endpointType, + useMultimodal); } Flux responseFlux = streamWithHttpClient(messages, tools, options); @@ -169,7 +211,7 @@ protected Flux doStream( private Flux streamWithHttpClient( List messages, List tools, GenerateOptions options) { Instant start = Instant.now(); - boolean useMultimodal = httpClient.requiresMultimodalApi(modelName); + boolean useMultimodal = httpClient.requiresMultimodalApi(modelName, endpointType); // Merge options with defaultOptions (options takes precedence) GenerateOptions effectiveOptions = GenerateOptions.mergeOptions(options, defaultOptions); @@ -218,6 +260,9 @@ private Flux streamWithHttpClient( // Apply thinking mode if enabled applyThinkingMode(request, effectiveOptions); + // Set endpoint type for endpoint selection + request.setEndpointType(endpointType); + if (stream) { // Streaming mode return httpClient.stream( @@ -298,6 +343,7 @@ public static class Builder { private boolean stream = true; private Boolean enableThinking; private Boolean enableSearch; + private EndpointType endpointType; private GenerateOptions defaultOptions = null; private String baseUrl; private Formatter formatter; @@ -318,12 +364,14 @@ public Builder apiKey(String apiKey) { /** * Sets the model name to use. * - *

The model name determines which API is used: + *

The model name determines which API is used when apiType is AUTO: *

* + *

Use {@link #endpointType(EndpointType)} to explicitly specify the endpoint type. + * * @param modelName the model name (e.g., "qwen-max", "qwen-vl-plus") * @return this builder instance */ @@ -374,6 +422,37 @@ public Builder enableSearch(Boolean enableSearch) { return this; } + /** + * Sets the endpoint type to use for endpoint routing. + * + *

This allows explicit control over which DashScope API endpoint to use: + *

+ * + *

Use this when the model name doesn't match the auto-detection patterns but + * you need to use a specific API. For example, qwen3.5-plus is a multimodal-capable + * model but its name doesn't match the auto-detection patterns. + * + *

Example: + *

{@code
+         * DashScopeChatModel model = DashScopeChatModel.builder()
+         *     .apiKey("sk-xxx")
+         *     .modelName("qwen3.5-plus")
+         *     .endpointType(EndpointType.MULTIMODAL)  // Force multimodal API for image inputs
+         *     .build();
+         * }
+ * + * @param endpointType the endpoint type to use (null for AUTO) + * @return this builder instance + */ + public Builder endpointType(EndpointType endpointType) { + this.endpointType = endpointType; + return this; + } + /** * Sets the default generation options. * @@ -503,6 +582,7 @@ public DashScopeChatModel build() { stream, enableThinking, enableSearch, + endpointType, effectiveOptions, baseUrl, formatter, diff --git a/agentscope-core/src/main/java/io/agentscope/core/model/DashScopeHttpClient.java b/agentscope-core/src/main/java/io/agentscope/core/model/DashScopeHttpClient.java index 5c2689fb1..fcf82d66f 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/model/DashScopeHttpClient.java +++ b/agentscope-core/src/main/java/io/agentscope/core/model/DashScopeHttpClient.java @@ -165,7 +165,8 @@ public DashScopeResponse call( Map additionalHeaders, Map additionalBodyParams, Map additionalQueryParams) { - String endpoint = selectEndpoint(request.getModel()); + EndpointType endpointType = request.getEndpointType() != null ? request.getEndpointType() : EndpointType.AUTO; + String endpoint = selectEndpoint(request.getModel(), endpointType); String url = buildUrl(endpoint, additionalQueryParams); try { @@ -236,7 +237,8 @@ public Flux stream( Map additionalHeaders, Map additionalBodyParams, Map additionalQueryParams) { - String endpoint = selectEndpoint(request.getModel()); + EndpointType endpointType = request.getEndpointType() != null ? request.getEndpointType() : EndpointType.AUTO; + String endpoint = selectEndpoint(request.getModel(), endpointType); String url = buildUrl(endpoint, additionalQueryParams); try { @@ -303,36 +305,107 @@ public Flux stream( /** * Select the appropriate API endpoint based on model name. * - *

Routing logic (consistent with existing SDK behavior): + *

This is a convenience method that uses {@link EndpointType#AUTO} for automatic detection. + * + * @param modelName the model name + * @return the API endpoint path + */ + public String selectEndpoint(String modelName) { + return selectEndpoint(modelName, EndpointType.AUTO); + } + + /** + * Select the appropriate API endpoint based on model name and endpoint type. + * + *

Routing logic: *

    - *
  • Models starting with "qvq" → multimodal API
  • - *
  • Models containing "-vl" → multimodal API
  • - *
  • All other models → text generation API
  • + *
  • If endpointType is {@link EndpointType#TEXT} → text generation API
  • + *
  • If endpointType is {@link EndpointType#MULTIMODAL} → multimodal API
  • + *
  • If endpointType is {@link EndpointType#AUTO}: + *
      + *
    • Models starting with "qvq" → multimodal API
    • + *
    • Models containing "-vl" → multimodal API
    • + *
    • All other models → text generation API
    • + *
    + *
  • *
* * @param modelName the model name + * @param endpointType the endpoint type to use (AUTO for automatic detection) * @return the API endpoint path */ - public String selectEndpoint(String modelName) { + public String selectEndpoint(String modelName, EndpointType endpointType) { + if (endpointType == EndpointType.TEXT) { + log.debug("Using text generation API (explicitly set) for model: {}", modelName); + return TEXT_GENERATION_ENDPOINT; + } + if (endpointType == EndpointType.MULTIMODAL) { + log.debug("Using multimodal API (explicitly set) for model: {}", modelName); + return MULTIMODAL_GENERATION_ENDPOINT; + } + // AUTO: use model name detection if (modelName == null) { return TEXT_GENERATION_ENDPOINT; } - if (modelName.startsWith("qvq") || modelName.contains("-vl")) { - log.debug("Using multimodal API for model: {}", modelName); + if (isMultimodalModel(modelName)) { + log.debug("Using multimodal API (auto-detected) for model: {}", modelName); return MULTIMODAL_GENERATION_ENDPOINT; } - log.debug("Using text generation API for model: {}", modelName); + log.debug("Using text generation API (auto-detected) for model: {}", modelName); return TEXT_GENERATION_ENDPOINT; } + /** + * Check if a model is a multimodal model that requires the multimodal-generation API. + * + *

Supported multimodal model patterns (used when endpointType is AUTO): + *

    + *
  • Models starting with "qvq" (e.g., qvq-72b, qvq-max)
  • + *
  • Models containing "-vl" (e.g., qwen-vl-plus, qwen3-vl-max)
  • + *
  • Models starting with "qwen3.5-plus" (e.g., qwen3.5-plus, qwen3.5-plus-2026-02-15)
  • + *
+ * + * @param modelName the model name + * @return true if the model is a multimodal model + */ + public static boolean isMultimodalModel(String modelName) { + if (modelName == null) { + return false; + } + String lowerModelName = modelName.toLowerCase(); + return lowerModelName.startsWith("qvq") + || lowerModelName.contains("-vl") + || lowerModelName.startsWith("qwen3.5-plus"); + } + /** * Check if a model requires the multimodal API. * + *

This is a convenience method that uses {@link EndpointType#AUTO} for automatic detection. + * * @param modelName the model name * @return true if the model requires multimodal API */ public boolean requiresMultimodalApi(String modelName) { - return MULTIMODAL_GENERATION_ENDPOINT.equals(selectEndpoint(modelName)); + return requiresMultimodalApi(modelName, EndpointType.AUTO); + } + + /** + * Check if a model requires the multimodal API based on the specified endpoint type. + * + * @param modelName the model name + * @param endpointType the endpoint type to use + * @return true if the model requires multimodal API + */ + public boolean requiresMultimodalApi(String modelName, EndpointType endpointType) { + if (endpointType == EndpointType.MULTIMODAL) { + return true; + } + if (endpointType == EndpointType.TEXT) { + return false; + } + // AUTO: use model name detection + return isMultimodalModel(modelName); } /** diff --git a/agentscope-core/src/main/java/io/agentscope/core/model/EndpointType.java b/agentscope-core/src/main/java/io/agentscope/core/model/EndpointType.java new file mode 100644 index 000000000..da7b53e95 --- /dev/null +++ b/agentscope-core/src/main/java/io/agentscope/core/model/EndpointType.java @@ -0,0 +1,62 @@ +/* + * Copyright 2024-2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.agentscope.core.model; + +/** + * Enum representing the endpoint type for DashScope models. + * + *

This allows developers to explicitly specify which API endpoint to use, + * overriding the automatic model name-based detection. + * + *

Usage example: + *

{@code
+ * DashScopeChatModel model = DashScopeChatModel.builder()
+ *     .apiKey("sk-xxx")
+ *     .modelName("qwen3.5-plus")
+ *     .endpointType(EndpointType.MULTIMODAL)  // Explicitly use multimodal API
+ *     .build();
+ * }
+ */ +public enum EndpointType { + /** + * Automatically determine endpoint type based on model name. + * + *

This is the default behavior. The routing logic: + *

    + *
  • Models starting with "qvq" → MULTIMODAL
  • + *
  • Models containing "-vl" → MULTIMODAL
  • + *
  • Models starting with "qwen3.5-plus" → MULTIMODAL
  • + *
  • All other models → TEXT
  • + *
+ */ + AUTO, + + /** + * Force use of text generation API. + * + *

Use this when you want to explicitly use the text-generation endpoint + * for a model that would otherwise be auto-detected as multimodal. + */ + TEXT, + + /** + * Force use of multimodal generation API. + * + *

Use this when you want to use a multimodal-capable model (like qwen3.5-plus) + * with image inputs, but the model name doesn't match the auto-detection patterns. + */ + MULTIMODAL +} From aa45d5762d047b933e77643726252643842de0e3 Mon Sep 17 00:00:00 2001 From: "qihuai.wyq" Date: Wed, 25 Feb 2026 16:03:43 +0800 Subject: [PATCH 2/6] format the code Change-Id: I40f87398172d9330db09dccebf1fe8ab0516c51a --- .../agentscope/core/model/DashScopeChatModel.java | 15 +++++++++++++-- .../core/model/DashScopeHttpClient.java | 6 ++++-- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/agentscope-core/src/main/java/io/agentscope/core/model/DashScopeChatModel.java b/agentscope-core/src/main/java/io/agentscope/core/model/DashScopeChatModel.java index 5c790d7e1..3e86c1e4f 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/model/DashScopeChatModel.java +++ b/agentscope-core/src/main/java/io/agentscope/core/model/DashScopeChatModel.java @@ -97,8 +97,19 @@ public DashScopeChatModel( HttpTransport httpTransport, String publicKeyId, String publicKey) { - this(apiKey, modelName, stream, enableThinking, enableSearch, null, - defaultOptions, baseUrl, formatter, httpTransport, publicKeyId, publicKey); + this( + apiKey, + modelName, + stream, + enableThinking, + enableSearch, + null, + defaultOptions, + baseUrl, + formatter, + httpTransport, + publicKeyId, + publicKey); } /** diff --git a/agentscope-core/src/main/java/io/agentscope/core/model/DashScopeHttpClient.java b/agentscope-core/src/main/java/io/agentscope/core/model/DashScopeHttpClient.java index fcf82d66f..d81b8472b 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/model/DashScopeHttpClient.java +++ b/agentscope-core/src/main/java/io/agentscope/core/model/DashScopeHttpClient.java @@ -165,7 +165,8 @@ public DashScopeResponse call( Map additionalHeaders, Map additionalBodyParams, Map additionalQueryParams) { - EndpointType endpointType = request.getEndpointType() != null ? request.getEndpointType() : EndpointType.AUTO; + EndpointType endpointType = + request.getEndpointType() != null ? request.getEndpointType() : EndpointType.AUTO; String endpoint = selectEndpoint(request.getModel(), endpointType); String url = buildUrl(endpoint, additionalQueryParams); @@ -237,7 +238,8 @@ public Flux stream( Map additionalHeaders, Map additionalBodyParams, Map additionalQueryParams) { - EndpointType endpointType = request.getEndpointType() != null ? request.getEndpointType() : EndpointType.AUTO; + EndpointType endpointType = + request.getEndpointType() != null ? request.getEndpointType() : EndpointType.AUTO; String endpoint = selectEndpoint(request.getModel(), endpointType); String url = buildUrl(endpoint, additionalQueryParams); From ffa533cec57c63814e33881ab78a62d4dc880098 Mon Sep 17 00:00:00 2001 From: "qihuai.wyq" Date: Wed, 25 Feb 2026 17:36:07 +0800 Subject: [PATCH 3/6] add ut Change-Id: I2020b04333c2d142142ee6be4b56eeb019494a74 --- .../core/model/DashScopeChatModelTest.java | 82 ++++++++++ .../core/model/DashScopeHttpClientTest.java | 141 ++++++++++++++++++ 2 files changed, 223 insertions(+) diff --git a/agentscope-core/src/test/java/io/agentscope/core/model/DashScopeChatModelTest.java b/agentscope-core/src/test/java/io/agentscope/core/model/DashScopeChatModelTest.java index c38f837b4..7d0ed1e06 100644 --- a/agentscope-core/src/test/java/io/agentscope/core/model/DashScopeChatModelTest.java +++ b/agentscope-core/src/test/java/io/agentscope/core/model/DashScopeChatModelTest.java @@ -155,6 +155,88 @@ void testThinkingModeWithBudget() { thinkingWithBudgetModel, "Model with thinking mode and budget should be created"); } + // ========== EndpointType Builder Tests ========== + + @Test + @DisplayName("Should create model with explicit EndpointType.MULTIMODAL") + void testBuilderWithEndpointTypeMultimodal() { + DashScopeChatModel model = + DashScopeChatModel.builder() + .apiKey(mockApiKey) + .modelName("qwen3.5-plus") + .endpointType(EndpointType.MULTIMODAL) + .build(); + + assertNotNull(model, "Model with MULTIMODAL endpoint type should be created"); + } + + @Test + @DisplayName("Should create model with explicit EndpointType.TEXT") + void testBuilderWithEndpointTypeText() { + DashScopeChatModel model = + DashScopeChatModel.builder() + .apiKey(mockApiKey) + .modelName("qwen-plus") + .endpointType(EndpointType.TEXT) + .build(); + + assertNotNull(model, "Model with TEXT endpoint type should be created"); + } + + @Test + @DisplayName("Should create model with EndpointType.AUTO (default behavior)") + void testBuilderWithEndpointTypeAuto() { + DashScopeChatModel model = + DashScopeChatModel.builder() + .apiKey(mockApiKey) + .modelName("qwen-plus") + .endpointType(EndpointType.AUTO) + .build(); + + assertNotNull(model, "Model with AUTO endpoint type should be created"); + } + + @Test + @DisplayName("Should create model without endpointType (defaults to AUTO)") + void testBuilderWithoutEndpointType() { + // This tests backward compatibility - not setting endpointType should still work + DashScopeChatModel model = + DashScopeChatModel.builder() + .apiKey(mockApiKey) + .modelName("qwen-plus") + .stream(true) + .build(); + + assertNotNull(model, "Model without explicit endpointType should be created"); + } + + // ========== Backward Compatible Constructor Tests ========== + + @Test + @DisplayName("Should create model using overloaded constructor without endpointType") + void testOverloadedConstructorWithoutEndpointType() { + // The overloaded constructor without endpointType should delegate to the full constructor + DashScopeChatModel model = + new DashScopeChatModel( + mockApiKey, "qwen-plus", true, null, null, + null, null, null, null, null, null); + + assertNotNull(model, "Model from overloaded constructor should be created"); + assertEquals("qwen-plus", model.getModelName()); + } + + @Test + @DisplayName("Should create model using full constructor with explicit endpointType") + void testFullConstructorWithEndpointType() { + DashScopeChatModel model = + new DashScopeChatModel( + mockApiKey, "qwen3.5-plus", true, null, null, + EndpointType.MULTIMODAL, null, null, null, null, null, null); + + assertNotNull(model, "Model from full constructor should be created"); + assertEquals("qwen3.5-plus", model.getModelName()); + } + // ========== Vision Model Tests ========== @Test diff --git a/agentscope-core/src/test/java/io/agentscope/core/model/DashScopeHttpClientTest.java b/agentscope-core/src/test/java/io/agentscope/core/model/DashScopeHttpClientTest.java index d91cce636..5e669f13d 100644 --- a/agentscope-core/src/test/java/io/agentscope/core/model/DashScopeHttpClientTest.java +++ b/agentscope-core/src/test/java/io/agentscope/core/model/DashScopeHttpClientTest.java @@ -120,6 +120,147 @@ void testSelectEndpointForNullModel() { assertEquals(DashScopeHttpClient.TEXT_GENERATION_ENDPOINT, client.selectEndpoint(null)); } + // ========== EndpointType Routing Tests ========== + + @Test + void testSelectEndpointWithExplicitText() { + // Explicit TEXT forces text endpoint regardless of model name + assertEquals( + DashScopeHttpClient.TEXT_GENERATION_ENDPOINT, + client.selectEndpoint("qwen-vl-plus", EndpointType.TEXT)); + assertEquals( + DashScopeHttpClient.TEXT_GENERATION_ENDPOINT, + client.selectEndpoint("qvq-72b", EndpointType.TEXT)); + assertEquals( + DashScopeHttpClient.TEXT_GENERATION_ENDPOINT, + client.selectEndpoint("qwen3.5-plus", EndpointType.TEXT)); + } + + @Test + void testSelectEndpointWithExplicitMultimodal() { + // Explicit MULTIMODAL forces multimodal endpoint regardless of model name + assertEquals( + DashScopeHttpClient.MULTIMODAL_GENERATION_ENDPOINT, + client.selectEndpoint("qwen-plus", EndpointType.MULTIMODAL)); + assertEquals( + DashScopeHttpClient.MULTIMODAL_GENERATION_ENDPOINT, + client.selectEndpoint("qwen-max", EndpointType.MULTIMODAL)); + assertEquals( + DashScopeHttpClient.MULTIMODAL_GENERATION_ENDPOINT, + client.selectEndpoint("qwen3.5-plus", EndpointType.MULTIMODAL)); + } + + @Test + void testSelectEndpointWithAutoFallsBackToModelNameDetection() { + // AUTO uses model name detection + assertEquals( + DashScopeHttpClient.TEXT_GENERATION_ENDPOINT, + client.selectEndpoint("qwen-plus", EndpointType.AUTO)); + assertEquals( + DashScopeHttpClient.MULTIMODAL_GENERATION_ENDPOINT, + client.selectEndpoint("qwen-vl-plus", EndpointType.AUTO)); + assertEquals( + DashScopeHttpClient.MULTIMODAL_GENERATION_ENDPOINT, + client.selectEndpoint("qvq-72b", EndpointType.AUTO)); + assertEquals( + DashScopeHttpClient.MULTIMODAL_GENERATION_ENDPOINT, + client.selectEndpoint("qwen3.5-plus", EndpointType.AUTO)); + } + + @Test + void testRequiresMultimodalApiWithEndpointType() { + // Explicit MULTIMODAL always returns true + assertTrue(client.requiresMultimodalApi("qwen-plus", EndpointType.MULTIMODAL)); + // Explicit TEXT always returns false + assertFalse(client.requiresMultimodalApi("qwen-vl-plus", EndpointType.TEXT)); + // AUTO falls back to model name detection + assertFalse(client.requiresMultimodalApi("qwen-plus", EndpointType.AUTO)); + assertTrue(client.requiresMultimodalApi("qwen-vl-plus", EndpointType.AUTO)); + assertTrue(client.requiresMultimodalApi("qwen3.5-plus", EndpointType.AUTO)); + } + + @Test + void testIsMultimodalModelIncludesQwen35Plus() { + // qwen3.5-plus is hardcoded as multimodal model + assertTrue(DashScopeHttpClient.isMultimodalModel("qwen3.5-plus")); + assertTrue(DashScopeHttpClient.isMultimodalModel("qwen3.5-plus-2026-02-15")); + // qwen-3.5-plus (with hyphen) does not match + assertFalse(DashScopeHttpClient.isMultimodalModel("qwen-3.5-plus")); + } + + @Test + void testIsMultimodalModelPatterns() { + // qvq prefix + assertTrue(DashScopeHttpClient.isMultimodalModel("qvq-72b")); + assertTrue(DashScopeHttpClient.isMultimodalModel("QVQ-MAX")); + // -vl pattern + assertTrue(DashScopeHttpClient.isMultimodalModel("qwen-vl-plus")); + assertTrue(DashScopeHttpClient.isMultimodalModel("qwen3-vl-max")); + // Not multimodal + assertFalse(DashScopeHttpClient.isMultimodalModel("qwen-plus")); + assertFalse(DashScopeHttpClient.isMultimodalModel("qwen-max")); + assertFalse(DashScopeHttpClient.isMultimodalModel(null)); + assertFalse(DashScopeHttpClient.isMultimodalModel("")); + } + + @Test + void testRequestEndpointTypePassedToEndpointSelection() throws Exception { + // Verify that endpointType in DashScopeRequest is used for endpoint routing + String responseJson = + """ + { + "request_id": "test", + "output": { + "choices": [{ + "message": { "role": "assistant", "content": "ok" }, + "finish_reason": "stop" + }] + } + } + """; + mockServer.enqueue( + new MockResponse() + .setResponseCode(200) + .setBody(responseJson) + .setHeader("Content-Type", "application/json")); + + DashScopeMessage userMsg = + DashScopeMessage.builder().role("user").content("Hello").build(); + DashScopeRequest request = + DashScopeRequest.builder() + .model("qwen-plus") + .input(new DashScopeInput(List.of(userMsg))) + .parameters(new DashScopeParameters()) + .endpointType(EndpointType.MULTIMODAL) + .build(); + + assertEquals(EndpointType.MULTIMODAL, request.getEndpointType()); + + DashScopeResponse response = client.call(request, null, null, null); + assertNotNull(response); + + // Verify the request was sent to the multimodal endpoint + var recorded = mockServer.takeRequest(); + assertTrue( + recorded.getPath() + .contains(DashScopeHttpClient.MULTIMODAL_GENERATION_ENDPOINT), + "Request should be sent to multimodal endpoint, but was: " + + recorded.getPath()); + } + + @Test + void testRequestEndpointTypeDefaultsToAuto() { + DashScopeRequest request = new DashScopeRequest(); + assertEquals(EndpointType.AUTO, request.getEndpointType()); + + DashScopeRequest request2 = + new DashScopeRequest("qwen-plus", new DashScopeInput(List.of()), null); + assertEquals(EndpointType.AUTO, request2.getEndpointType()); + + DashScopeRequest request3 = DashScopeRequest.builder().model("qwen-plus").build(); + assertEquals(EndpointType.AUTO, request3.getEndpointType()); + } + @Test void testCallTextGenerationApi() throws Exception { String responseJson = From a39315057c7d1e60c7a746697257b323ff2f9675 Mon Sep 17 00:00:00 2001 From: "qihuai.wyq" Date: Wed, 25 Feb 2026 17:39:42 +0800 Subject: [PATCH 4/6] format the code Change-Id: I122cae15d91cc685cccc4b2387eac707f17245bf --- .../core/model/DashScopeChatModelTest.java | 32 ++++++++++++++----- .../core/model/DashScopeHttpClientTest.java | 9 ++---- 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/agentscope-core/src/test/java/io/agentscope/core/model/DashScopeChatModelTest.java b/agentscope-core/src/test/java/io/agentscope/core/model/DashScopeChatModelTest.java index 7d0ed1e06..42195de0a 100644 --- a/agentscope-core/src/test/java/io/agentscope/core/model/DashScopeChatModelTest.java +++ b/agentscope-core/src/test/java/io/agentscope/core/model/DashScopeChatModelTest.java @@ -201,10 +201,7 @@ void testBuilderWithEndpointTypeAuto() { void testBuilderWithoutEndpointType() { // This tests backward compatibility - not setting endpointType should still work DashScopeChatModel model = - DashScopeChatModel.builder() - .apiKey(mockApiKey) - .modelName("qwen-plus") - .stream(true) + DashScopeChatModel.builder().apiKey(mockApiKey).modelName("qwen-plus").stream(true) .build(); assertNotNull(model, "Model without explicit endpointType should be created"); @@ -218,8 +215,17 @@ void testOverloadedConstructorWithoutEndpointType() { // The overloaded constructor without endpointType should delegate to the full constructor DashScopeChatModel model = new DashScopeChatModel( - mockApiKey, "qwen-plus", true, null, null, - null, null, null, null, null, null); + mockApiKey, + "qwen-plus", + true, + null, + null, + null, + null, + null, + null, + null, + null); assertNotNull(model, "Model from overloaded constructor should be created"); assertEquals("qwen-plus", model.getModelName()); @@ -230,8 +236,18 @@ void testOverloadedConstructorWithoutEndpointType() { void testFullConstructorWithEndpointType() { DashScopeChatModel model = new DashScopeChatModel( - mockApiKey, "qwen3.5-plus", true, null, null, - EndpointType.MULTIMODAL, null, null, null, null, null, null); + mockApiKey, + "qwen3.5-plus", + true, + null, + null, + EndpointType.MULTIMODAL, + null, + null, + null, + null, + null, + null); assertNotNull(model, "Model from full constructor should be created"); assertEquals("qwen3.5-plus", model.getModelName()); diff --git a/agentscope-core/src/test/java/io/agentscope/core/model/DashScopeHttpClientTest.java b/agentscope-core/src/test/java/io/agentscope/core/model/DashScopeHttpClientTest.java index 5e669f13d..d3ef6dff1 100644 --- a/agentscope-core/src/test/java/io/agentscope/core/model/DashScopeHttpClientTest.java +++ b/agentscope-core/src/test/java/io/agentscope/core/model/DashScopeHttpClientTest.java @@ -224,8 +224,7 @@ void testRequestEndpointTypePassedToEndpointSelection() throws Exception { .setBody(responseJson) .setHeader("Content-Type", "application/json")); - DashScopeMessage userMsg = - DashScopeMessage.builder().role("user").content("Hello").build(); + DashScopeMessage userMsg = DashScopeMessage.builder().role("user").content("Hello").build(); DashScopeRequest request = DashScopeRequest.builder() .model("qwen-plus") @@ -242,10 +241,8 @@ void testRequestEndpointTypePassedToEndpointSelection() throws Exception { // Verify the request was sent to the multimodal endpoint var recorded = mockServer.takeRequest(); assertTrue( - recorded.getPath() - .contains(DashScopeHttpClient.MULTIMODAL_GENERATION_ENDPOINT), - "Request should be sent to multimodal endpoint, but was: " - + recorded.getPath()); + recorded.getPath().contains(DashScopeHttpClient.MULTIMODAL_GENERATION_ENDPOINT), + "Request should be sent to multimodal endpoint, but was: " + recorded.getPath()); } @Test From db0246c890224f3093a2ebc81668397bdda8c555 Mon Sep 17 00:00:00 2001 From: "qihuai.wyq" Date: Thu, 26 Feb 2026 14:10:25 +0800 Subject: [PATCH 5/6] update the comment and multimodal check Change-Id: I11d18dfb4adc57a38f123a7a966704da9fafa64b --- .../core/model/DashScopeChatModel.java | 38 +++---------------- .../core/model/DashScopeHttpClient.java | 5 ++- .../agentscope/core/model/EndpointType.java | 16 ++------ .../core/model/DashScopeHttpClientTest.java | 8 ++-- 4 files changed, 17 insertions(+), 50 deletions(-) diff --git a/agentscope-core/src/main/java/io/agentscope/core/model/DashScopeChatModel.java b/agentscope-core/src/main/java/io/agentscope/core/model/DashScopeChatModel.java index 3e86c1e4f..4991665c4 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/model/DashScopeChatModel.java +++ b/agentscope-core/src/main/java/io/agentscope/core/model/DashScopeChatModel.java @@ -37,11 +37,8 @@ *

This implementation uses direct HTTP calls to DashScope API via OkHttp, * without depending on the DashScope Java SDK. * - *

Supports both text and vision models through automatic endpoint routing: - *

    - *
  • Vision models (names starting with "qvq" or containing "-vl") use MultiModalGeneration API - *
  • Text models use TextGeneration API - *
+ *

Supports both text and vision models through automatic endpoint routing. + * Use {@link EndpointType} to explicitly control the endpoint selection. * *

Features: *

    @@ -375,16 +372,11 @@ public Builder apiKey(String apiKey) { /** * Sets the model name to use. * - *

    The model name determines which API is used when apiType is AUTO: - *

      - *
    • Vision models (qvq* or *-vl*) → MultiModal API
    • - *
    • Text models → Text Generation API
    • - *
    - * - *

    Use {@link #endpointType(EndpointType)} to explicitly specify the endpoint type. + *

    The model name determines which API is used when endpointType is AUTO. * * @param modelName the model name (e.g., "qwen-max", "qwen-vl-plus") * @return this builder instance + * @see DashScopeHttpClient#isMultimodalModel(String) */ public Builder modelName(String modelName) { this.modelName = modelName; @@ -436,28 +428,10 @@ public Builder enableSearch(Boolean enableSearch) { /** * Sets the endpoint type to use for endpoint routing. * - *

    This allows explicit control over which DashScope API endpoint to use: - *

      - *
    • {@link EndpointType#AUTO} - Automatic detection based on model name (default)
    • - *
    • {@link EndpointType#TEXT} - Force use of text-generation API
    • - *
    • {@link EndpointType#MULTIMODAL} - Force use of multimodal-generation API
    • - *
    - * - *

    Use this when the model name doesn't match the auto-detection patterns but - * you need to use a specific API. For example, qwen3.5-plus is a multimodal-capable - * model but its name doesn't match the auto-detection patterns. - * - *

    Example: - *

    {@code
    -         * DashScopeChatModel model = DashScopeChatModel.builder()
    -         *     .apiKey("sk-xxx")
    -         *     .modelName("qwen3.5-plus")
    -         *     .endpointType(EndpointType.MULTIMODAL)  // Force multimodal API for image inputs
    -         *     .build();
    -         * }
    - * * @param endpointType the endpoint type to use (null for AUTO) * @return this builder instance + * @see EndpointType + * @see DashScopeHttpClient#isMultimodalModel(String) */ public Builder endpointType(EndpointType endpointType) { this.endpointType = endpointType; diff --git a/agentscope-core/src/main/java/io/agentscope/core/model/DashScopeHttpClient.java b/agentscope-core/src/main/java/io/agentscope/core/model/DashScopeHttpClient.java index d81b8472b..f6e2c72fc 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/model/DashScopeHttpClient.java +++ b/agentscope-core/src/main/java/io/agentscope/core/model/DashScopeHttpClient.java @@ -327,6 +327,7 @@ public String selectEndpoint(String modelName) { *
      *
    • Models starting with "qvq" → multimodal API
    • *
    • Models containing "-vl" → multimodal API
    • + *
    • Models starting with "qwen3.5" → multimodal API
    • *
    • All other models → text generation API
    • *
    * @@ -364,7 +365,7 @@ public String selectEndpoint(String modelName, EndpointType endpointType) { *
      *
    • Models starting with "qvq" (e.g., qvq-72b, qvq-max)
    • *
    • Models containing "-vl" (e.g., qwen-vl-plus, qwen3-vl-max)
    • - *
    • Models starting with "qwen3.5-plus" (e.g., qwen3.5-plus, qwen3.5-plus-2026-02-15)
    • + *
    • Models starting with "qwen3.5" (e.g., qwen3.5-plus, qwen3.5-flash)
    • *
    * * @param modelName the model name @@ -377,7 +378,7 @@ public static boolean isMultimodalModel(String modelName) { String lowerModelName = modelName.toLowerCase(); return lowerModelName.startsWith("qvq") || lowerModelName.contains("-vl") - || lowerModelName.startsWith("qwen3.5-plus"); + || lowerModelName.startsWith("qwen3.5"); } /** diff --git a/agentscope-core/src/main/java/io/agentscope/core/model/EndpointType.java b/agentscope-core/src/main/java/io/agentscope/core/model/EndpointType.java index da7b53e95..5e1f51de9 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/model/EndpointType.java +++ b/agentscope-core/src/main/java/io/agentscope/core/model/EndpointType.java @@ -34,29 +34,19 @@ public enum EndpointType { /** * Automatically determine endpoint type based on model name. * - *

    This is the default behavior. The routing logic: - *

      - *
    • Models starting with "qvq" → MULTIMODAL
    • - *
    • Models containing "-vl" → MULTIMODAL
    • - *
    • Models starting with "qwen3.5-plus" → MULTIMODAL
    • - *
    • All other models → TEXT
    • - *
    + *

    This is the default behavior. + * + * @see DashScopeHttpClient#isMultimodalModel(String) */ AUTO, /** * Force use of text generation API. - * - *

    Use this when you want to explicitly use the text-generation endpoint - * for a model that would otherwise be auto-detected as multimodal. */ TEXT, /** * Force use of multimodal generation API. - * - *

    Use this when you want to use a multimodal-capable model (like qwen3.5-plus) - * with image inputs, but the model name doesn't match the auto-detection patterns. */ MULTIMODAL } diff --git a/agentscope-core/src/test/java/io/agentscope/core/model/DashScopeHttpClientTest.java b/agentscope-core/src/test/java/io/agentscope/core/model/DashScopeHttpClientTest.java index d3ef6dff1..36aec2c2d 100644 --- a/agentscope-core/src/test/java/io/agentscope/core/model/DashScopeHttpClientTest.java +++ b/agentscope-core/src/test/java/io/agentscope/core/model/DashScopeHttpClientTest.java @@ -180,11 +180,13 @@ void testRequiresMultimodalApiWithEndpointType() { } @Test - void testIsMultimodalModelIncludesQwen35Plus() { - // qwen3.5-plus is hardcoded as multimodal model + void testIsMultimodalModelIncludesQwen35Family() { + // Qwen 3.5 family is entirely multimodal (prefix-based matching) assertTrue(DashScopeHttpClient.isMultimodalModel("qwen3.5-plus")); assertTrue(DashScopeHttpClient.isMultimodalModel("qwen3.5-plus-2026-02-15")); - // qwen-3.5-plus (with hyphen) does not match + assertTrue(DashScopeHttpClient.isMultimodalModel("qwen3.5-flash")); + assertTrue(DashScopeHttpClient.isMultimodalModel("qwen3.5-397b-a17b")); + // qwen-3.5-plus (with hyphen before 3.5) does not match assertFalse(DashScopeHttpClient.isMultimodalModel("qwen-3.5-plus")); } From 53a69cd4b01e28ecc51c197d54e1e9b075b207c9 Mon Sep 17 00:00:00 2001 From: "qihuai.wyq" Date: Thu, 26 Feb 2026 14:27:58 +0800 Subject: [PATCH 6/6] add 'asr' as multimodal check Change-Id: Iaace1bd32b2e6f86dcbb8efa4580ac1f0fbdf734 --- .../java/io/agentscope/core/model/DashScopeHttpClient.java | 3 +++ .../java/io/agentscope/core/model/DashScopeHttpClientTest.java | 2 ++ 2 files changed, 5 insertions(+) diff --git a/agentscope-core/src/main/java/io/agentscope/core/model/DashScopeHttpClient.java b/agentscope-core/src/main/java/io/agentscope/core/model/DashScopeHttpClient.java index f6e2c72fc..678d0ee9a 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/model/DashScopeHttpClient.java +++ b/agentscope-core/src/main/java/io/agentscope/core/model/DashScopeHttpClient.java @@ -327,6 +327,7 @@ public String selectEndpoint(String modelName) { *

      *
    • Models starting with "qvq" → multimodal API
    • *
    • Models containing "-vl" → multimodal API
    • + *
    • Models containing "-asr" → multimodal API
    • *
    • Models starting with "qwen3.5" → multimodal API
    • *
    • All other models → text generation API
    • *
    @@ -365,6 +366,7 @@ public String selectEndpoint(String modelName, EndpointType endpointType) { *
      *
    • Models starting with "qvq" (e.g., qvq-72b, qvq-max)
    • *
    • Models containing "-vl" (e.g., qwen-vl-plus, qwen3-vl-max)
    • + *
    • Models containing "-asr" (e.g., qwen3-asr-flash)
    • *
    • Models starting with "qwen3.5" (e.g., qwen3.5-plus, qwen3.5-flash)
    • *
    * @@ -378,6 +380,7 @@ public static boolean isMultimodalModel(String modelName) { String lowerModelName = modelName.toLowerCase(); return lowerModelName.startsWith("qvq") || lowerModelName.contains("-vl") + || lowerModelName.contains("-asr") || lowerModelName.startsWith("qwen3.5"); } diff --git a/agentscope-core/src/test/java/io/agentscope/core/model/DashScopeHttpClientTest.java b/agentscope-core/src/test/java/io/agentscope/core/model/DashScopeHttpClientTest.java index 36aec2c2d..1b3be8dad 100644 --- a/agentscope-core/src/test/java/io/agentscope/core/model/DashScopeHttpClientTest.java +++ b/agentscope-core/src/test/java/io/agentscope/core/model/DashScopeHttpClientTest.java @@ -198,6 +198,8 @@ void testIsMultimodalModelPatterns() { // -vl pattern assertTrue(DashScopeHttpClient.isMultimodalModel("qwen-vl-plus")); assertTrue(DashScopeHttpClient.isMultimodalModel("qwen3-vl-max")); + // -asr pattern + assertTrue(DashScopeHttpClient.isMultimodalModel("qwen3-asr-flash")); // Not multimodal assertFalse(DashScopeHttpClient.isMultimodalModel("qwen-plus")); assertFalse(DashScopeHttpClient.isMultimodalModel("qwen-max"));