diff --git a/TelegramSearchBot.LLM.Test/Service/AI/LLM/McpToolHelperProxyTests.cs b/TelegramSearchBot.LLM.Test/Service/AI/LLM/McpToolHelperProxyTests.cs
index 2fee59d5..b002424b 100644
--- a/TelegramSearchBot.LLM.Test/Service/AI/LLM/McpToolHelperProxyTests.cs
+++ b/TelegramSearchBot.LLM.Test/Service/AI/LLM/McpToolHelperProxyTests.cs
@@ -1,9 +1,11 @@
using TelegramSearchBot.Model;
using TelegramSearchBot.Model.AI;
using TelegramSearchBot.Service.AI.LLM;
+using TelegramSearchBot.Test;
using Xunit;
namespace TelegramSearchBot.LLM.Test.Service.AI.LLM {
+ [Collection(McpToolHelperTestCollection.Name)]
public class McpToolHelperProxyTests {
[Fact]
public async Task ExecuteRegisteredToolAsync_ProxyTool_ForwardsToolContextMetadata() {
@@ -46,5 +48,48 @@ public async Task ExecuteRegisteredToolAsync_ProxyTool_ForwardsToolContextMetada
Assert.Equal("456", forwardedArguments["__userId"]);
Assert.Equal("789", forwardedArguments["__messageId"]);
}
+
+ [Fact]
+ public void TryParseToolCalls_ProxyToolXml_ParsesRegisteredProxyTool() {
+ var toolName = $"proxy_parse_test_{Guid.NewGuid():N}";
+
+ McpToolHelper.RegisterProxyTools([
+ new ProxyToolDefinition {
+ Name = toolName,
+ Description = "Proxy search tool.",
+ Parameters = [
+ new ProxyToolParameter {
+ Name = "query",
+ Type = "string",
+ Description = "Query text.",
+ Required = true
+ },
+ new ProxyToolParameter {
+ Name = "count",
+ Type = "integer",
+ Description = "Result count.",
+ Required = false
+ }
+ ]
+ }
+ ], (_, _) => Task.FromResult("ok"));
+
+ var xml = $"""
+
+
+ 苏州今日天气
+ 5
+
+
+""";
+
+ var parsed = McpToolHelper.TryParseToolCalls(xml, out var parsedToolCalls);
+
+ Assert.True(parsed);
+ Assert.Single(parsedToolCalls);
+ Assert.Equal(toolName, parsedToolCalls[0].toolName);
+ Assert.Equal("苏州今日天气", parsedToolCalls[0].arguments["query"]);
+ Assert.Equal("5", parsedToolCalls[0].arguments["count"]);
+ }
}
}
diff --git a/TelegramSearchBot.LLM.Test/Service/AI/LLM/OpenAIToolCallNormalizationTests.cs b/TelegramSearchBot.LLM.Test/Service/AI/LLM/OpenAIToolCallNormalizationTests.cs
index f82c3938..53683e09 100644
--- a/TelegramSearchBot.LLM.Test/Service/AI/LLM/OpenAIToolCallNormalizationTests.cs
+++ b/TelegramSearchBot.LLM.Test/Service/AI/LLM/OpenAIToolCallNormalizationTests.cs
@@ -1,4 +1,6 @@
using TelegramSearchBot.Service.AI.LLM;
+using TelegramSearchBot.Model.AI;
+using TelegramSearchBot.Model.Data;
using Xunit;
namespace TelegramSearchBot.LLM.Test.Service.AI.LLM {
@@ -61,5 +63,35 @@ public void DeserializeToolArgumentsForDisplay_InvalidJson_ReturnsEmptyDictionar
Assert.Empty(arguments);
}
+
+ [Fact]
+ public void IsMiniMaxCompatibleEndpoint_MiniMaxProvider_ReturnsTrue() {
+ var channel = new LLMChannel {
+ Provider = LLMProvider.MiniMax,
+ Gateway = "https://api.minimaxi.com/v1"
+ };
+
+ Assert.True(OpenAIService.IsMiniMaxCompatibleEndpoint(channel, "MiniMax-M2.7"));
+ }
+
+ [Fact]
+ public void IsMiniMaxCompatibleEndpoint_MiniMaxGateway_ReturnsTrue() {
+ var channel = new LLMChannel {
+ Provider = LLMProvider.OpenAI,
+ Gateway = "https://api.minimaxi.com/v1"
+ };
+
+ Assert.True(OpenAIService.IsMiniMaxCompatibleEndpoint(channel, "some-model"));
+ }
+
+ [Fact]
+ public void IsMiniMaxCompatibleEndpoint_OpenAIProvider_ReturnsFalse() {
+ var channel = new LLMChannel {
+ Provider = LLMProvider.OpenAI,
+ Gateway = "https://api.openai.com/v1"
+ };
+
+ Assert.False(OpenAIService.IsMiniMaxCompatibleEndpoint(channel, "gpt-4.1"));
+ }
}
}
diff --git a/TelegramSearchBot.LLM/Service/AI/LLM/McpToolHelper.cs b/TelegramSearchBot.LLM/Service/AI/LLM/McpToolHelper.cs
index cf26d59c..d580ba3d 100644
--- a/TelegramSearchBot.LLM/Service/AI/LLM/McpToolHelper.cs
+++ b/TelegramSearchBot.LLM/Service/AI/LLM/McpToolHelper.cs
@@ -484,7 +484,7 @@ public static bool TryParseToolCalls(string input, out List<(string toolName, Di
// Check if there's a wrapper element containing tool elements
if (doc.Root.Elements().Any(e =>
e.Name.LocalName.Equals("tool", StringComparison.OrdinalIgnoreCase) ||
- ToolRegistry.ContainsKey(e.Name.LocalName))) {
+ IsToolRegistered(e.Name.LocalName))) {
// Process each tool element inside the wrapper
foreach (var toolElement in doc.Root.Elements()) {
var toolCall = ParseToolElement(toolElement);
@@ -500,11 +500,18 @@ public static bool TryParseToolCalls(string input, out List<(string toolName, Di
// Fallback to regex parsing for individual tool blocks
var toolBlockMatches = Regex.Matches(processedInput, @"<(tool\b[^>]*|[\w]+)(?:\s[^>]*)?>[\s\S]*?<\/\1>", RegexOptions.IgnoreCase);
- _sLogger?.LogInformation($"TryParseToolCalls: Found {toolBlockMatches.Count} tool elements in input: {processedInput}");
- _sLogger?.LogInformation($"TryParseToolCalls: Raw regex matches - Count: {toolBlockMatches.Count}");
- _sLogger?.LogInformation($"TryParseToolCalls: Using regex pattern: {@"<(tool\b[^>]*|[\w]+)(?:\s[^>]*)?>[\s\S]*?<\/\1>"}");
+ _sLogger?.LogInformation(
+ "TryParseToolCalls: Found {ToolBlockCount} potential tool elements. BuiltInCount={BuiltInCount}, ExternalCount={ExternalCount}, ProxyCount={ProxyCount}, AvailableTools={AvailableTools}, InputPreview={InputPreview}",
+ toolBlockMatches.Count,
+ ToolRegistry.Count,
+ ExternalToolRegistry.Count,
+ ProxyToolRegistry.Count,
+ GetAvailableToolNamesForLog(),
+ TruncateForLog(processedInput, MaxToolLogPayloadLength));
+ _sLogger?.LogDebug($"TryParseToolCalls: Raw regex matches - Count: {toolBlockMatches.Count}");
+ _sLogger?.LogTrace($"TryParseToolCalls: Using regex pattern: {@"<(tool\b[^>]*|[\w]+)(?:\s[^>]*)?>[\s\S]*?<\/\1>"}");
for (int i = 0; i < toolBlockMatches.Count; i++) {
- _sLogger?.LogInformation($"Match {i}: {toolBlockMatches[i].Value}");
+ _sLogger?.LogTrace($"Match {i}: {toolBlockMatches[i].Value}");
}
_sLogger?.LogDebug($"TryParseToolCalls: Found {toolBlockMatches.Count} potential tool blocks using regex.");
@@ -549,27 +556,30 @@ public static bool TryParseToolCalls(string input, out List<(string toolName, Di
private static (string toolName, Dictionary arguments) ParseToolElement(XElement element) {
string toolName = null;
+ var requestedToolName = element.Name.LocalName;
var arguments = new Dictionary(StringComparer.OrdinalIgnoreCase);
// 确定工具名称 - 更灵活的匹配逻辑
if (element.Name.LocalName.Equals("tool", StringComparison.OrdinalIgnoreCase)) {
- toolName = element.Attribute("name")?.Value;
- if (string.IsNullOrEmpty(toolName)) {
+ requestedToolName = element.Attribute("name")?.Value;
+ if (string.IsNullOrEmpty(requestedToolName)) {
_sLogger?.LogWarning("ParseToolElement: Tool element has no name attribute");
return (null, null);
}
+ toolName = ResolveRegisteredToolName(requestedToolName);
} else {
- // 尝试匹配注册的工具名称 (built-in and external)
- toolName = ToolRegistry.Keys.FirstOrDefault(k =>
- k.Equals(element.Name.LocalName, StringComparison.OrdinalIgnoreCase));
- if (toolName == null) {
- toolName = ExternalToolRegistry.Keys.FirstOrDefault(k =>
- k.Equals(element.Name.LocalName, StringComparison.OrdinalIgnoreCase));
- }
+ // 尝试匹配注册的工具名称 (built-in, external, or proxy)
+ toolName = ResolveRegisteredToolName(requestedToolName);
}
- if (toolName == null || ( !ToolRegistry.ContainsKey(toolName) && !ExternalToolRegistry.ContainsKey(toolName) )) {
- _sLogger?.LogWarning($"ParseToolElement: Unregistered tool '{element.Name.LocalName}'");
+ if (toolName == null) {
+ _sLogger?.LogWarning(
+ "ParseToolElement: Unregistered tool '{ToolName}'. BuiltInCount={BuiltInCount}, ExternalCount={ExternalCount}, ProxyCount={ProxyCount}, AvailableTools={AvailableTools}",
+ requestedToolName,
+ ToolRegistry.Count,
+ ExternalToolRegistry.Count,
+ ProxyToolRegistry.Count,
+ GetAvailableToolNamesForLog());
return (null, null);
}
@@ -698,6 +708,16 @@ public static async Task