Repository: agentscope-ai/agentscope-java (2026-07-16T15:14:32Z)

Issue:
[Feature]: Anthropic disable_parallel_tool_use 配置缺失
框架有 parallelToolCalls 通用字段但未映射 Anthropic tool choice。


Discussion:
I want to try to solve this, but I'm not sure if that's possible



Patch:
diff --git a/agentscope-extensions/agentscope-extensions-model/agentscope-extensions-model-anthropic/src/main/java/io/agentscope/extensions/model/anthropic/formatter/AnthropicToolsHelper.java b/agentscope-extensions/agentscope-extensions-model/agentscope-extensions-model-anthropic/src/main/java/io/agentscope/extensions/model/anthropic/formatter/AnthropicToolsHelper.java
index e59d847cfa..0e9fe8908a 100644
--- a/agentscope-extensions/agentscope-extensions-model/agentscope-extensions-model-anthropic/src/main/java/io/agentscope/extensions/model/anthropic/formatter/AnthropicToolsHelper.java
+++ b/agentscope-extensions/agentscope-extensions-model/agentscope-extensions-model-anthropic/src/main/java/io/agentscope/extensions/model/anthropic/formatter/AnthropicToolsHelper.java
@@ -69,9 +69,18 @@ public static void applyTools(
             builder.addTool(tool);
         }
 
-        // Apply tool choice if specified
-        if (options != null && options.getToolChoice() != null) {
-            applyToolChoice(builder, options.getToolChoice());
+        // Resolve effective parallelToolCalls and toolChoice
+        Boolean parallelToolCalls = options != null ? options.getParallelToolCalls() : null;
+        ToolChoice toolChoice = options != null ? options.getToolChoice() : null;
+
+        if (toolChoice != null) {
+            // Explicit tool choice — pass parallelToolCalls along
+            applyToolChoice(builder, toolChoice, parallelToolCalls);
+        } else if (parallelToolCalls != null) {
+            // No explicit tool choice, but parallelToolCalls is set —
+            // Anthropic requires disable_parallel_tool_use to be inside a tool_choice object,
+            // so we create an implicit Auto with disableParallelToolUse
+            applyToolChoice(builder, new ToolChoice.Auto(), parallelToolCalls);
         }
     }
 
@@ -89,12 +98,32 @@ private static JsonValue convertToJsonValue(Object parameters) {
 
     /**
      * Apply tool choice to the builder.
+     *
+     * <p>Anthropic has no top-level {@code parallel_tool_calls} field. Instead, parallel tool
+     * use is controlled via the {@code disable_parallel_tool_use} sub-field inside the {@code
+     * tool_choice} object. The mapping is {@code disable_parallel_tool_use = !parallelToolCalls}.
+     * {@link ToolChoiceNone} does not support this sub-field, so it is ignored in that case.
+     *
+     * @param parallelToolCalls the effective {@code parallelToolCalls} option, or {@code null} to
+     *     leave Anthropic's default behavior unchanged
      */
     private static void applyToolChoice(
-            MessageCreateParams.Builder builder, ToolChoice toolChoice) {
+            MessageCreateParams.Builder builder, ToolChoice toolChoice, Boolean parallelToolCalls) {
+        boolean disableParallel = parallelToolCalls != null && !parallelToolCalls;
+
         if (toolChoice instanceof ToolChoice.Auto) {
-            builder.toolChoice(ofAuto(ToolChoiceAuto.builder().build()));
+            ToolChoiceAuto.Builder tcBuilder = ToolChoiceAuto.builder();
+            if (parallelToolCalls != null) {
+                tcBuilder.disableParallelToolUse(disableParallel);
+            }
+            builder.toolChoice(ofAuto(tcBuilder.build()));
         } else if (toolChoice instanceof ToolChoice.None) {
+            // ToolChoiceNone does not support disable_parallel_tool_use
+            if (parallelToolCalls != null && disableParallel) {
+                log.debug(
+                        "disable_parallel_tool_use is not supported with ToolChoice.None,"
+                                + " ignoring");
+            }
             builder.toolChoice(ofNone(ToolChoiceNone.builder().build()));
         } else if (toolChoice instanceof ToolChoice.Required) {
             // Anthropic doesn't have a direct "required" option, use "any" which forces tool
@@ -102,9 +131,17 @@ private static void applyToolChoice(
             log.warn(
                     "Anthropic API doesn't support ToolChoice.Required directly, using 'any'"
                             + " instead");
-            builder.toolChoice(ofAny(ToolChoiceAny.builder().build()));
+            ToolChoiceAny.Builder tcBuilder = ToolChoiceAny.builder();
+            if (parallelToolCalls != null) {
+                tcBuilder.disableParallelToolUse(disableParallel);
+            }
+            builder.toolChoice(ofAny(tcBuilder.build()));
         } else if (toolChoice instanceof ToolChoice.Specific specific) {
-            builder.toolChoice(ofTool(ToolChoiceTool.builder().name(specific.toolName()).build()));
+            ToolChoiceTool.Builder tcBuilder = ToolChoiceTool.builder().name(specific.toolName());
+            if (parallelToolCalls != null) {
+                tcBuilder.disableParallelToolUse(disableParallel);
+            }
+            builder.toolChoice(ofTool(tcBuilder.build()));
         } else {
             log.warn("Unknown tool choice type: {}", toolChoice);
         }


Test patch:
diff --git a/agentscope-extensions/agentscope-extensions-model/agentscope-extensions-model-anthropic/src/test/java/io/agentscope/extensions/model/anthropic/formatter/AnthropicToolsHelperTest.java b/agentscope-extensions/agentscope-extensions-model/agentscope-extensions-model-anthropic/src/test/java/io/agentscope/extensions/model/anthropic/formatter/AnthropicToolsHelperTest.java
index d2636f741a..3bea33b551 100644
--- a/agentscope-extensions/agentscope-extensions-model/agentscope-extensions-model-anthropic/src/test/java/io/agentscope/extensions/model/anthropic/formatter/AnthropicToolsHelperTest.java
+++ b/agentscope-extensions/agentscope-extensions-model/agentscope-extensions-model-anthropic/src/test/java/io/agentscope/extensions/model/anthropic/formatter/AnthropicToolsHelperTest.java
@@ -222,6 +222,183 @@ void testApplyToolChoiceSpecific() {
         assertEquals("search", params.toolChoice().get().asTool().name());
     }
 
+    @Test
+    void testApplyToolChoiceAutoWithParallelDisabled() {
+        MessageCreateParams.Builder builder = createBuilder();
+
+        ToolSchema schema =
+                ToolSchema.builder()
+                        .name("search")
+                        .description("Search")
+                        .parameters(Map.of("type", "object"))
+                        .build();
+
+        GenerateOptions options =
+                GenerateOptions.builder()
+                        .toolChoice(new ToolChoice.Auto())
+                        .parallelToolCalls(false)
+                        .build();
+        AnthropicToolsHelper.applyTools(builder, List.of(schema), options);
+
+        MessageCreateParams params = builder.build();
+        assertTrue(params.toolChoice().isPresent());
+        assertTrue(params.toolChoice().get().isAuto());
+        // parallelToolCalls=false -> disable_parallel_tool_use=true
+        assertTrue(params.toolChoice().get().asAuto().disableParallelToolUse().isPresent());
+        assertTrue(params.toolChoice().get().asAuto().disableParallelToolUse().get());
+    }
+
+    @Test
+    void testApplyToolChoiceAutoWithParallelEnabled() {
+        MessageCreateParams.Builder builder = createBuilder();
+
+        ToolSchema schema =
+                ToolSchema.builder()
+                        .name("search")
+                        .description("Search")
+                        .parameters(Map.of("type", "object"))
+                        .build();
+
+        GenerateOptions options =
+                GenerateOptions.builder()
+                        .toolChoice(new ToolChoice.Auto())
+                        .parallelToolCalls(true)
+                        .build();
+        AnthropicToolsHelper.applyTools(builder, List.of(schema), options);
+
+        MessageCreateParams params = builder.build();
+        assertTrue(params.toolChoice().isPresent());
+        assertTrue(params.toolChoice().get().isAuto());
+        // parallelToolCalls=true -> disable_parallel_tool_use=false
+        assertTrue(params.toolChoice().get().asAuto().disableParallelToolUse().isPresent());
+        assertTrue(!params.toolChoice().get().asAuto().disableParallelToolUse().get());
+    }
+
+    @Test
+    void testApplyToolChoiceRequiredWithParallelDisabled() {
+        MessageCreateParams.Builder builder = createBuilder();
+
+        ToolSchema schema =
+                ToolSchema.builder()
+                        .name("search")
+                        .description("Search")
+                        .parameters(Map.of("type", "object"))
+                        .build();
+
+        GenerateOptions options =
+                GenerateOptions.builder()
+                        .toolChoice(new ToolChoice.Required())
+                        .parallelToolCalls(false)
+                        .build();
+        AnthropicToolsHelper.applyTools(builder, List.of(schema), options);
+
+        MessageCreateParams params = builder.build();
+        assertTrue(params.toolChoice().isPresent());
+        assertTrue(params.toolChoice().get().isAny());
+        // Required maps to "any"; disable_parallel_tool_use=true
+        assertTrue(params.toolChoice().get().asAny().disableParallelToolUse().isPresent());
+        assertTrue(params.toolChoice().get().asAny().disableParallelToolUse().get());
+    }
+
+    @Test
+    void testApplyToolChoiceSpecificWithParallelDisabled() {
+        MessageCreateParams.Builder builder = createBuilder();
+
+        ToolSchema schema =
+                ToolSchema.builder()
+                        .name("search")
+                        .description("Search")
+                        .parameters(Map.of("type", "object"))
+                        .build();
+
+        GenerateOptions options =
+                GenerateOptions.builder()
+                        .toolChoice(new ToolChoice.Specific("search"))
+                        .parallelToolCalls(false)
+                        .build();
+        AnthropicToolsHelper.applyTools(builder, List.of(schema), options);
+
+        MessageCreateParams params = builder.build();
+        assertTrue(params.toolChoice().isPresent());
+        assertTrue(params.toolChoice().get().isTool());
+        assertEquals("search", params.toolChoice().get().asTool().name());
+        // disable_parallel_tool_use=true
+        assertTrue(params.toolChoice().get().asTool().disableParallelToolUse().isPresent());
+        assertTrue(params.toolChoice().get().asTool().disableParallelToolUse().get());
+    }
+
+    @Test
+    void testApplyToolChoiceNoneWithParallelDisabled() {
+        MessageCreateParams.Builder builder = createBuilder();
+
+        ToolSchema schema =
+                ToolSchema.builder()
+                        .name("search")
+                        .description("Search")
+                        .parameters(Map.of("type", "object"))
+                        .build();
+
+        GenerateOptions options =
+                GenerateOptions.builder()
+                        .toolChoice(new ToolChoice.None())
+                        .parallelToolCalls(false)
+                        .build();
+        AnthropicToolsHelper.applyTools(builder, List.of(schema), options);
+
+        MessageCreateParams params = builder.build();
+        assertTrue(params.toolChoice().isPresent());
+        // None correctly maps to "none"; disable_parallel_tool_use is not supported and ignored
+        assertTrue(params.toolChoice().get().isNone());
+        assertTrue(!params.toolChoice().get().isAny());
+        assertTrue(!params.toolChoice().get().isAuto());
+    }
+
+    @Test
+    void testApplyToolsParallelDisabledWithoutToolChoice() {
+        MessageCreateParams.Builder builder = createBuilder();
+
+        ToolSchema schema =
+                ToolSchema.builder()
+                        .name("search")
+                        .description("Search")
+                        .parameters(Map.of("type", "object"))
+                        .build();
+
+        // Only parallelToolCalls is set, no explicit toolChoice
+        GenerateOptions options = GenerateOptions.builder().parallelToolCalls(false).build();
+        AnthropicToolsHelper.applyTools(builder, List.of(schema), options);
+
+        MessageCreateParams params = builder.build();
+        // An implicit "auto" tool_choice is created to carry disable_parallel_tool_use
+        assertTrue(params.toolChoice().isPresent());
+        assertTrue(params.toolChoice().get().isAuto());
+        assertTrue(params.toolChoice().get().asAuto().disableParallelToolUse().isPresent());
+        assertTrue(params.toolChoice().get().asAuto().disableParallelToolUse().get());
+    }
+
+    @Test
+    void testApplyToolChoiceAutoWithoutParallel() {
+        MessageCreateParams.Builder builder = createBuilder();
+
+        ToolSchema schema =
+                ToolSchema.builder()
+                        .name("search")
+                        .description("Search")
+                        .parameters(Map.of("type", "object"))
+                        .build();
+
+        // toolChoice set, but parallelToolCalls left null (default behavior)
+        GenerateOptions options =
+                GenerateOptions.builder().toolChoice(new ToolChoice.Auto()).build();
+        AnthropicToolsHelper.applyTools(builder, List.of(schema), options);
+
+        MessageCreateParams params = builder.build();
+        assertTrue(params.toolChoice().isPresent());
+        assertTrue(params.toolChoice().get().isAuto());
+        // No disable_parallel_tool_use set -> Anthropic default (parallel enabled)
+        assertTrue(params.toolChoice().get().asAuto().disableParallelToolUse().isEmpty());
+    }
+
     @Test
     void testApplyOptionsWithTemperature() {
         MessageCreateParams.Builder builder = createBuilder();