From 3c1543a1049670c227755229a0305613442bcda8 Mon Sep 17 00:00:00 2001
From: Junjie <fallin.jie@qq.com>
Date: 星期三, 29 四月 2026 20:43:13 +0800
Subject: [PATCH] Merge branch 'codex/ai-provider-protocol-gateway'

---
 src/test/java/com/zy/ai/service/AutoTuneCoordinatorServiceImplTest.java |  281 ++++++++++++++++++++++++++++++++++++++++++++++++++++++--
 1 files changed, 270 insertions(+), 11 deletions(-)

diff --git a/src/test/java/com/zy/ai/service/AutoTuneCoordinatorServiceImplTest.java b/src/test/java/com/zy/ai/service/AutoTuneCoordinatorServiceImplTest.java
index 2121fdb..0b245a1 100644
--- a/src/test/java/com/zy/ai/service/AutoTuneCoordinatorServiceImplTest.java
+++ b/src/test/java/com/zy/ai/service/AutoTuneCoordinatorServiceImplTest.java
@@ -1,6 +1,7 @@
 package com.zy.ai.service;
 
 import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.core.conditions.AbstractWrapper;
 import com.baomidou.mybatisplus.core.conditions.Wrapper;
 import com.zy.ai.domain.autotune.AutoTuneApplyRequest;
 import com.zy.ai.domain.autotune.AutoTuneApplyResult;
@@ -17,6 +18,7 @@
 import com.zy.ai.mcp.service.SpringAiMcpToolManager;
 import com.zy.ai.mcp.tool.AutoTuneMcpTools;
 import com.zy.ai.service.impl.AutoTuneAgentServiceImpl;
+import com.zy.ai.service.impl.AutoTuneControlModeServiceImpl;
 import com.zy.ai.service.impl.AutoTuneCoordinatorServiceImpl;
 import com.zy.asrs.service.WrkMastService;
 import com.zy.common.utils.RedisUtil;
@@ -44,6 +46,7 @@
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertSame;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -54,6 +57,7 @@
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.lenient;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
@@ -82,6 +86,7 @@
     private AiPromptTemplateService aiPromptTemplateService;
     @Mock
     private ConfigService configService;
+    private AutoTuneControlModeService autoTuneControlModeService;
     @Mock
     private WrkMastService wrkMastService;
     @Mock
@@ -93,12 +98,15 @@
 
     @BeforeEach
     void setUp() {
+        autoTuneControlModeService = new AutoTuneControlModeServiceImpl(configService);
         tools = new AutoTuneMcpTools(
                 autoTuneSnapshotService,
                 autoTuneApplyService,
                 aiAutoTuneJobService,
                 aiAutoTuneChangeService,
                 aiAutoTuneMcpCallService);
+        lenient().when(configService.getConfigValue("aiAutoTuneEnabled", "N")).thenReturn("Y");
+        lenient().when(configService.getConfigValue("aiAutoTuneAnalysisOnly", "Y")).thenReturn("N");
     }
 
     @Test
@@ -121,12 +129,19 @@
         job.setSummary("applied");
         job.setSuccessCount(1);
         job.setRejectCount(0);
-        AiAutoTuneChange change = new AiAutoTuneChange();
-        change.setJobId(70L);
-        change.setTargetType("sys_config");
-        change.setTargetKey("conveyorStationTaskLimit");
-        change.setRequestedValue("12");
-        change.setResultStatus("success");
+        AiAutoTuneChange agentChange = new AiAutoTuneChange();
+        agentChange.setJobId(7L);
+        agentChange.setTargetType("station");
+        agentChange.setTargetId("101");
+        agentChange.setTargetKey("outTaskLimit");
+        agentChange.setRequestedValue("3");
+        agentChange.setResultStatus("success");
+        AiAutoTuneChange applyChange = new AiAutoTuneChange();
+        applyChange.setJobId(70L);
+        applyChange.setTargetType("sys_config");
+        applyChange.setTargetKey("conveyorStationTaskLimit");
+        applyChange.setRequestedValue("12");
+        applyChange.setResultStatus("success");
         com.zy.ai.entity.AiAutoTuneMcpCall mcpCall = new com.zy.ai.entity.AiAutoTuneMcpCall();
         mcpCall.setAgentJobId(7L);
         mcpCall.setCallSeq(1);
@@ -136,7 +151,10 @@
 
         when(aiAutoTuneJobService.list(any(Wrapper.class))).thenReturn(Collections.singletonList(job));
         when(aiAutoTuneMcpCallService.list(any(Wrapper.class))).thenReturn(Collections.singletonList(mcpCall));
-        when(aiAutoTuneChangeService.list(any(Wrapper.class))).thenReturn(Collections.singletonList(change));
+        List<AiAutoTuneChange> auditChanges = new ArrayList<>();
+        auditChanges.add(agentChange);
+        auditChanges.add(applyChange);
+        when(aiAutoTuneChangeService.list(any(Wrapper.class))).thenReturn(auditChanges);
 
         List<Map<String, Object>> result = tools.getRecentAutoTuneJobs(99);
 
@@ -145,11 +163,54 @@
         assertFalse(result.get(0).containsKey("reasoningDigest"));
         assertEquals(1, result.get(0).get("mcpCallCount"));
         List<?> changes = (List<?>) result.get(0).get("changes");
-        assertEquals(1, changes.size());
+        assertEquals(2, changes.size());
+        assertEquals(7L, ((Map<?, ?>) changes.get(0)).get("jobId"));
+        assertEquals(70L, ((Map<?, ?>) changes.get(1)).get("jobId"));
 
         ArgumentCaptor<Wrapper<AiAutoTuneJob>> wrapperCaptor = ArgumentCaptor.forClass(Wrapper.class);
         verify(aiAutoTuneJobService).list(wrapperCaptor.capture());
         assertTrue(wrapperCaptor.getValue().getSqlSegment().contains("limit 20"));
+
+        ArgumentCaptor<Wrapper<AiAutoTuneChange>> changeWrapperCaptor = ArgumentCaptor.forClass(Wrapper.class);
+        verify(aiAutoTuneChangeService).list(changeWrapperCaptor.capture());
+        Wrapper<AiAutoTuneChange> changeWrapper = changeWrapperCaptor.getValue();
+        assertTrue(changeWrapper.getSqlSegment().contains("job_id IN"));
+        List<Object> changeQueryParams = wrapperParamValues(changeWrapper);
+        assertTrue(changeQueryParams.contains(7L));
+        assertTrue(changeQueryParams.contains(70L));
+    }
+
+    @Test
+    void recentJobsMarksRollbackMcpChangesAsRollback() {
+        AiAutoTuneJob job = new AiAutoTuneJob();
+        job.setId(8L);
+        job.setTriggerType("manual");
+        job.setStatus("success");
+        job.setSuccessCount(1);
+        job.setRejectCount(0);
+        AiAutoTuneChange rollbackChange = new AiAutoTuneChange();
+        rollbackChange.setJobId(80L);
+        rollbackChange.setTargetType("sys_config");
+        rollbackChange.setTargetKey("conveyorStationTaskLimit");
+        rollbackChange.setResultStatus("success");
+        com.zy.ai.entity.AiAutoTuneMcpCall mcpCall = new com.zy.ai.entity.AiAutoTuneMcpCall();
+        mcpCall.setAgentJobId(8L);
+        mcpCall.setCallSeq(1);
+        mcpCall.setToolName("wcs_local_dispatch_revert_last_auto_tune_job");
+        mcpCall.setStatus("success");
+        mcpCall.setApplyJobId(80L);
+        mcpCall.setSuccessCount(1);
+
+        when(aiAutoTuneJobService.list(any(Wrapper.class))).thenReturn(Collections.singletonList(job));
+        when(aiAutoTuneMcpCallService.list(any(Wrapper.class))).thenReturn(Collections.singletonList(mcpCall));
+        when(aiAutoTuneChangeService.list(any(Wrapper.class))).thenReturn(Collections.singletonList(rollbackChange));
+
+        List<Map<String, Object>> result = tools.getRecentAutoTuneJobs(1);
+
+        assertEquals("rollback", result.get(0).get("writeBehavior"));
+        List<?> changes = (List<?>) result.get(0).get("changes");
+        assertEquals(1, changes.size());
+        assertEquals("rollback", ((Map<?, ?>) changes.get(0)).get("writeBehavior"));
     }
 
     @Test
@@ -157,6 +218,7 @@
         AutoTuneApplyResult expected = new AutoTuneApplyResult();
         expected.setDryRun(true);
         expected.setSuccess(true);
+        expected.setChanges(Collections.singletonList(applyResultChange("dry_run")));
         when(autoTuneApplyService.apply(any(AutoTuneApplyRequest.class))).thenReturn(expected);
 
         AutoTuneChangeCommand command = new AutoTuneChangeCommand();
@@ -203,10 +265,38 @@
     }
 
     @Test
+    void applyToolRejectsDuplicateDryRunTargetsBeforeServiceCall() {
+        List<AutoTuneChangeCommand> changes = new ArrayList<>();
+        changes.add(change(" sys_config ", "ignored-first", " conveyorStationTaskLimit ", "12"));
+        changes.add(change("SYS_CONFIG", "ignored-second", "conveyorStationTaskLimit", "13"));
+
+        IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
+                () -> tools.applyAutoTuneChanges("duplicate target", 10, "agent", true, null, changes));
+
+        assertTrue(exception.getMessage().contains("Duplicate auto-tune change target"));
+        assertTrue(exception.getMessage().contains("targetType=sys_config"));
+        assertTrue(exception.getMessage().contains("targetKey=conveyorStationTaskLimit"));
+        verify(autoTuneApplyService, never()).apply(any(AutoTuneApplyRequest.class));
+    }
+
+    @Test
+    void applyToolRejectsRealApplyWithoutDryRunTokenInAnalysisOnlyMode() {
+        AutoTuneChangeCommand command = change("sys_config", null, "conveyorStationTaskLimit", "12");
+
+        IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
+                () -> tools.applyAutoTuneChanges("direct apply", 10, "agent", false, null,
+                        Collections.singletonList(command)));
+
+        assertTrue(exception.getMessage().contains("dryRunToken is required"));
+        verify(autoTuneApplyService, never()).apply(any(AutoTuneApplyRequest.class));
+    }
+
+    @Test
     void applyToolAllowsRealApplyOnlyWithMatchingDryRunToken() {
         AutoTuneApplyResult dryRunResult = new AutoTuneApplyResult();
         dryRunResult.setDryRun(true);
         dryRunResult.setSuccess(true);
+        dryRunResult.setChanges(Collections.singletonList(applyResultChange("dry_run")));
         AutoTuneApplyResult applyResult = new AutoTuneApplyResult();
         applyResult.setDryRun(false);
         applyResult.setSuccess(true);
@@ -230,6 +320,7 @@
         AutoTuneApplyResult dryRunResult = new AutoTuneApplyResult();
         dryRunResult.setDryRun(true);
         dryRunResult.setSuccess(true);
+        dryRunResult.setChanges(Collections.singletonList(applyResultChange("dry_run")));
         when(autoTuneApplyService.apply(any(AutoTuneApplyRequest.class))).thenReturn(dryRunResult);
 
         AutoTuneApplyResult preview = tools.applyAutoTuneChanges("preview", 10, "agent", true, null,
@@ -250,6 +341,7 @@
         AutoTuneApplyResult dryRunResult = new AutoTuneApplyResult();
         dryRunResult.setDryRun(true);
         dryRunResult.setSuccess(true);
+        dryRunResult.setChanges(Collections.singletonList(applyResultChange("dry_run")));
         when(autoTuneApplyService.apply(any(AutoTuneApplyRequest.class))).thenReturn(dryRunResult);
         List<AutoTuneChangeCommand> changes = Collections.singletonList(
                 change("sys_config", null, "conveyorStationTaskLimit", "12"));
@@ -264,8 +356,40 @@
     }
 
     @Test
+    void applyToolDoesNotIssueDryRunTokenWhenAllChangesAreNoChange() {
+        AutoTuneApplyResult dryRunResult = new AutoTuneApplyResult();
+        dryRunResult.setDryRun(true);
+        dryRunResult.setSuccess(true);
+        dryRunResult.setSuccessCount(0);
+        dryRunResult.setRejectCount(0);
+        dryRunResult.setChanges(Collections.singletonList(applyResultChange("no_change")));
+        when(autoTuneApplyService.apply(any(AutoTuneApplyRequest.class))).thenReturn(dryRunResult);
+
+        AutoTuneApplyResult preview = tools.applyAutoTuneChanges("preview", 10, "agent", true, null,
+                Collections.singletonList(change("sys_config", null, "conveyorStationTaskLimit", "12")));
+
+        assertNull(preview.getDryRunToken());
+        verify(autoTuneApplyService, times(1)).apply(any(AutoTuneApplyRequest.class));
+    }
+
+    @Test
     void rollbackToolDelegatesToApplyServiceRollback() {
         AutoTuneApplyResult expected = new AutoTuneApplyResult();
+        when(autoTuneApplyService.rollbackLastSuccessfulJob("bad result")).thenReturn(expected);
+
+        AutoTuneApplyResult result = tools.revertLastAutoTuneJob("bad result");
+
+        assertSame(expected, result);
+        verify(autoTuneApplyService).rollbackLastSuccessfulJob("bad result");
+    }
+
+    @Test
+    void rollbackToolBlocksRollbackInAnalysisOnlyMode() {
+        AutoTuneApplyResult expected = new AutoTuneApplyResult();
+        expected.setSuccess(false);
+        expected.setAnalysisOnly(true);
+        expected.setNoApply(true);
+        expected.setRejectCount(1);
         when(autoTuneApplyService.rollbackLastSuccessfulJob("bad result")).thenReturn(expected);
 
         AutoTuneApplyResult result = tools.revertLastAutoTuneJob("bad result");
@@ -563,7 +687,8 @@
         AutoTuneAgentServiceImpl service = new AutoTuneAgentServiceImpl(
                 llmChatService,
                 mcpToolManager,
-                aiPromptTemplateService);
+                aiPromptTemplateService,
+                autoTuneControlModeService);
         AiPromptTemplate promptTemplate = new AiPromptTemplate();
         promptTemplate.setContent("system prompt");
         when(aiPromptTemplateService.resolvePublished("wcs_auto_tune_dispatch")).thenReturn(promptTemplate);
@@ -658,6 +783,72 @@
     }
 
     @Test
+    void agentCallsMcpForRealApplyInAnalysisOnlyMode() {
+        when(configService.getConfigValue("aiAutoTuneAnalysisOnly", "Y")).thenReturn("Y");
+        AutoTuneAgentServiceImpl service = agentService();
+        when(mcpToolManager.buildOpenAiTools()).thenReturn(allowedOpenAiTools());
+        when(mcpToolManager.callTool(any(), any(JSONObject.class))).thenAnswer(invocation -> {
+            String toolName = invocation.getArgument(0);
+            if ("wcs_local_dispatch_apply_auto_tune_changes".equals(toolName)) {
+                return rejectedAnalysisOnlyResult(false);
+            }
+            return Collections.singletonMap("ok", true);
+        });
+        when(llmChatService.chatCompletionOrThrow(any(), anyDouble(), anyInt(), any()))
+                .thenReturn(
+                        response("snapshot", toolCall("call_1", "wcs_local_dispatch_get_auto_tune_snapshot",
+                                "{}"), 10, 5),
+                        response("apply", toolCall("call_2", "wcs_local_dispatch_apply_auto_tune_changes",
+                                "{\"dryRun\":false,\"changes\":[{\"targetType\":\"sys_config\",\"targetKey\":\"conveyorStationTaskLimit\",\"newValue\":\"12\"}]}"), 10, 5),
+                        response("stop", null, 10, 5)
+                );
+
+        AutoTuneAgentService.AutoTuneAgentResult result = service.runAutoTune("manual");
+
+        assertFalse(result.getSuccess());
+        assertTrue(result.getAnalysisOnly());
+        assertFalse(result.getAllowApply());
+        assertEquals("analysis_only", result.getExecutionMode());
+        assertFalse(result.getActualApplyCalled());
+        assertEquals(1, result.getRejectCount());
+        assertTrue(result.getSummary().contains("浠呭垎鏋愭ā寮忕姝㈠疄闄呭簲鐢�/鍥炴粴"));
+        verify(mcpToolManager).callTool(eq("wcs_local_dispatch_get_auto_tune_snapshot"), any(JSONObject.class));
+        verify(mcpToolManager).callTool(eq("wcs_local_dispatch_apply_auto_tune_changes"), any(JSONObject.class));
+    }
+
+    @Test
+    void agentCallsMcpForRollbackInAnalysisOnlyMode() {
+        when(configService.getConfigValue("aiAutoTuneAnalysisOnly", "Y")).thenReturn("Y");
+        AutoTuneAgentServiceImpl service = agentService();
+        when(mcpToolManager.buildOpenAiTools()).thenReturn(allowedOpenAiTools());
+        when(mcpToolManager.callTool(any(), any(JSONObject.class))).thenAnswer(invocation -> {
+            String toolName = invocation.getArgument(0);
+            if ("wcs_local_dispatch_revert_last_auto_tune_job".equals(toolName)) {
+                return rejectedAnalysisOnlyResult(false);
+            }
+            return Collections.singletonMap("ok", true);
+        });
+        when(llmChatService.chatCompletionOrThrow(any(), anyDouble(), anyInt(), any()))
+                .thenReturn(
+                        response("snapshot", toolCall("call_1", "wcs_local_dispatch_get_auto_tune_snapshot",
+                                "{}"), 10, 5),
+                        response("rollback", toolCall("call_2", "wcs_local_dispatch_revert_last_auto_tune_job",
+                                "{\"reason\":\"bad result\"}"), 10, 5),
+                        response("stop", null, 10, 5)
+                );
+
+        AutoTuneAgentService.AutoTuneAgentResult result = service.runAutoTune("manual");
+
+        assertFalse(result.getSuccess());
+        assertTrue(result.getAnalysisOnly());
+        assertTrue(result.getRollbackCalled());
+        assertEquals(1, result.getRejectCount());
+        assertTrue(result.getSummary().contains("浠呭垎鏋愭ā寮忕姝㈠疄闄呭簲鐢�/鍥炴粴"));
+        verify(mcpToolManager).callTool(eq("wcs_local_dispatch_get_auto_tune_snapshot"), any(JSONObject.class));
+        verify(mcpToolManager).callTool(eq("wcs_local_dispatch_revert_last_auto_tune_job"), any(JSONObject.class));
+    }
+
+    @Test
     void agentMarksSnapshotOnlyRunAsNoActualMutationEvenIfAssistantClaimsApplied() {
         AutoTuneAgentServiceImpl service = agentService();
         when(mcpToolManager.buildOpenAiTools()).thenReturn(allowedOpenAiTools());
@@ -674,7 +865,44 @@
         assertFalse(result.getActualApplyCalled());
         assertFalse(result.getRollbackCalled());
         assertEquals(0, result.getSuccessCount());
-        assertTrue(result.getSummary().startsWith("鑷姩璋冨弬 Agent 鏈皟鐢ㄥ疄闄呭簲鐢ㄦ垨鍥炴粴宸ュ叿锛屾湭淇敼杩愯鍙傛暟銆�"));
+        assertTrue(result.getSummary().contains("鑷姩璋冨弬 Agent 鏈皟鐢ㄥ疄闄呭簲鐢ㄦ垨鍥炴粴宸ュ叿锛屾湭淇敼杩愯鍙傛暟銆�"));
+    }
+
+    @Test
+    void agentDoesNotMarkNoChangeRealApplyAsActualApplyWhenSuccessCountIsPositive() {
+        AutoTuneAgentServiceImpl service = agentService();
+        when(mcpToolManager.buildOpenAiTools()).thenReturn(allowedOpenAiTools());
+        when(mcpToolManager.callTool(any(), any(JSONObject.class))).thenAnswer(invocation -> {
+            String toolName = invocation.getArgument(0);
+            if ("wcs_local_dispatch_apply_auto_tune_changes".equals(toolName)) {
+                LinkedHashMap<String, Object> noChange = new LinkedHashMap<>();
+                noChange.put("resultStatus", "no_change");
+
+                LinkedHashMap<String, Object> applyOutput = new LinkedHashMap<>();
+                applyOutput.put("success", true);
+                applyOutput.put("dryRun", false);
+                applyOutput.put("successCount", 1);
+                applyOutput.put("rejectCount", 0);
+                applyOutput.put("changes", Collections.singletonList(noChange));
+                return applyOutput;
+            }
+            return Collections.singletonMap("ok", true);
+        });
+        when(llmChatService.chatCompletionOrThrow(any(), anyDouble(), anyInt(), any()))
+                .thenReturn(
+                        response("snapshot", toolCall("call_1", "wcs_local_dispatch_get_auto_tune_snapshot",
+                                "{}"), 10, 5),
+                        response("apply no change", toolCall("call_2", "wcs_local_dispatch_apply_auto_tune_changes",
+                                "{\"dryRun\":false,\"dryRunToken\":\"token-123\",\"changes\":[{\"targetType\":\"sys_config\",\"targetKey\":\"conveyorStationTaskLimit\",\"newValue\":\"12\"}]}"), 10, 5),
+                        response("鏃犲彉鏇�", null, 10, 5)
+                );
+
+        AutoTuneAgentService.AutoTuneAgentResult result = service.runAutoTune("manual");
+
+        assertTrue(result.getSuccess());
+        assertFalse(result.getActualApplyCalled());
+        assertEquals(1, result.getSuccessCount());
+        assertEquals(0, result.getRejectCount());
     }
 
     @Test
@@ -820,12 +1048,17 @@
         AiPromptTemplate promptTemplate = new AiPromptTemplate();
         promptTemplate.setContent("system prompt");
         when(aiPromptTemplateService.resolvePublished("wcs_auto_tune_dispatch")).thenReturn(promptTemplate);
-        return new AutoTuneAgentServiceImpl(llmChatService, mcpToolManager, aiPromptTemplateService);
+        return new AutoTuneAgentServiceImpl(
+                llmChatService,
+                mcpToolManager,
+                aiPromptTemplateService,
+                autoTuneControlModeService);
     }
 
     private AutoTuneCoordinatorServiceImpl coordinatorService() {
         return new AutoTuneCoordinatorServiceImpl(
                 configService,
+                autoTuneControlModeService,
                 wrkMastService,
                 aiAutoTuneJobService,
                 aiAutoTuneMcpCallService,
@@ -883,6 +1116,19 @@
         return result;
     }
 
+    private AutoTuneApplyResult rejectedAnalysisOnlyResult(Boolean dryRun) {
+        AutoTuneApplyResult result = new AutoTuneApplyResult();
+        result.setDryRun(dryRun);
+        result.setSuccess(false);
+        result.setAnalysisOnly(true);
+        result.setNoApply(true);
+        result.setSuccessCount(0);
+        result.setRejectCount(1);
+        result.setSummary("浠呭垎鏋愭ā寮忕姝㈠疄闄呭簲鐢�/鍥炴粴锛屾湭淇敼杩愯鍙傛暟");
+        result.setChanges(new ArrayList<>());
+        return result;
+    }
+
     private AutoTuneChangeCommand change(String targetType, String targetId, String targetKey, String newValue) {
         AutoTuneChangeCommand command = new AutoTuneChangeCommand();
         command.setTargetType(targetType);
@@ -892,6 +1138,19 @@
         return command;
     }
 
+    private AiAutoTuneChange applyResultChange(String resultStatus) {
+        AiAutoTuneChange change = new AiAutoTuneChange();
+        change.setResultStatus(resultStatus);
+        return change;
+    }
+
+    private List<Object> wrapperParamValues(Wrapper<?> wrapper) {
+        if (!(wrapper instanceof AbstractWrapper<?, ?, ?> abstractWrapper)) {
+            return Collections.emptyList();
+        }
+        return new ArrayList<>(abstractWrapper.getParamNameValuePairs().values());
+    }
+
     private List<Object> allowedOpenAiTools() {
         List<Object> tools = new ArrayList<>();
         tools.add(openAiTool("wcs_local_dispatch_get_auto_tune_snapshot"));

--
Gitblit v1.9.1