From cbe283c28ffb7e788e6b8b77a8c016878fa824dc Mon Sep 17 00:00:00 2001 From: octo-patch <266937838+octo-patch@users.noreply.github.com> Date: Thu, 9 Jul 2026 08:28:51 +0000 Subject: [PATCH 1/2] Add MiniMax provider support --- BotSharp.sln | 15 +++++++++++ .../BotSharp.Plugin.MiniMaxAI.csproj | 17 +++++++++++++ .../MiniMaxAiPlugin.cs | 18 +++++++++++++ .../Providers/Chat/ChatCompletionProvider.cs | 15 +++++++++++ .../Providers/Text/TextCompletionProvider.cs | 13 ++++++++++ .../BotSharp.Plugin.MiniMaxAI/Using.cs | 8 ++++++ src/WebStarter/WebStarter.csproj | 1 + src/WebStarter/appsettings.json | 25 +++++++++++++++++++ 8 files changed, 112 insertions(+) create mode 100644 src/Plugins/BotSharp.Plugin.MiniMaxAI/BotSharp.Plugin.MiniMaxAI.csproj create mode 100644 src/Plugins/BotSharp.Plugin.MiniMaxAI/MiniMaxAiPlugin.cs create mode 100644 src/Plugins/BotSharp.Plugin.MiniMaxAI/Providers/Chat/ChatCompletionProvider.cs create mode 100644 src/Plugins/BotSharp.Plugin.MiniMaxAI/Providers/Text/TextCompletionProvider.cs create mode 100644 src/Plugins/BotSharp.Plugin.MiniMaxAI/Using.cs diff --git a/BotSharp.sln b/BotSharp.sln index 31ce5133e..78db00ed4 100644 --- a/BotSharp.sln +++ b/BotSharp.sln @@ -125,6 +125,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Core.Rules", "src\ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Plugin.DeepSeekAI", "src\Plugins\BotSharp.Plugin.DeepSeekAI\BotSharp.Plugin.DeepSeekAI.csproj", "{AF329442-B48E-4B48-A18A-1C869D1BA6F5}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Plugin.MiniMaxAI", "src\Plugins\BotSharp.Plugin.MiniMaxAI\BotSharp.Plugin.MiniMaxAI.csproj", "{D92C1A49-3F64-4D0C-91C0-B14EA47D2503}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Core.Realtime", "src\Infrastructure\BotSharp.Core.Realtime\BotSharp.Core.Realtime.csproj", "{781F1465-365C-0F22-1775-25025DAFA4C7}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Test.ComputerUse", "tests\BotSharp.Test.ComputerUse\BotSharp.Test.ComputerUse.csproj", "{C19D9AC1-97DD-8E65-E8DB-D295A095AA2D}" @@ -737,6 +739,18 @@ Global {AF329442-B48E-4B48-A18A-1C869D1BA6F5}.Release|x64.Build.0 = Release|Any CPU {AF329442-B48E-4B48-A18A-1C869D1BA6F5}.Release|x86.ActiveCfg = Release|Any CPU {AF329442-B48E-4B48-A18A-1C869D1BA6F5}.Release|x86.Build.0 = Release|Any CPU + {D92C1A49-3F64-4D0C-91C0-B14EA47D2503}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D92C1A49-3F64-4D0C-91C0-B14EA47D2503}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D92C1A49-3F64-4D0C-91C0-B14EA47D2503}.Debug|x64.ActiveCfg = Debug|Any CPU + {D92C1A49-3F64-4D0C-91C0-B14EA47D2503}.Debug|x64.Build.0 = Debug|Any CPU + {D92C1A49-3F64-4D0C-91C0-B14EA47D2503}.Debug|x86.ActiveCfg = Debug|Any CPU + {D92C1A49-3F64-4D0C-91C0-B14EA47D2503}.Debug|x86.Build.0 = Debug|Any CPU + {D92C1A49-3F64-4D0C-91C0-B14EA47D2503}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D92C1A49-3F64-4D0C-91C0-B14EA47D2503}.Release|Any CPU.Build.0 = Release|Any CPU + {D92C1A49-3F64-4D0C-91C0-B14EA47D2503}.Release|x64.ActiveCfg = Release|Any CPU + {D92C1A49-3F64-4D0C-91C0-B14EA47D2503}.Release|x64.Build.0 = Release|Any CPU + {D92C1A49-3F64-4D0C-91C0-B14EA47D2503}.Release|x86.ActiveCfg = Release|Any CPU + {D92C1A49-3F64-4D0C-91C0-B14EA47D2503}.Release|x86.Build.0 = Release|Any CPU {781F1465-365C-0F22-1775-25025DAFA4C7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {781F1465-365C-0F22-1775-25025DAFA4C7}.Debug|Any CPU.Build.0 = Debug|Any CPU {781F1465-365C-0F22-1775-25025DAFA4C7}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -1001,6 +1015,7 @@ Global {F812BAAE-5A7D-4DF7-8E71-70696B51C61F} = {E29DC6C4-5E57-48C5-BCB0-6B8F84782749} {AFD64412-4D6A-452E-82A2-79E5D8842E29} = {E29DC6C4-5E57-48C5-BCB0-6B8F84782749} {AF329442-B48E-4B48-A18A-1C869D1BA6F5} = {D5293208-2BEF-42FC-A64C-5954F61720BA} + {D92C1A49-3F64-4D0C-91C0-B14EA47D2503} = {D5293208-2BEF-42FC-A64C-5954F61720BA} {781F1465-365C-0F22-1775-25025DAFA4C7} = {E29DC6C4-5E57-48C5-BCB0-6B8F84782749} {C19D9AC1-97DD-8E65-E8DB-D295A095AA2D} = {32FAFFFE-A4CB-4FEE-BF7C-84518BBC6DCC} {B268E2F0-060F-8466-7D81-ABA4D735CA59} = {51AFE054-AE99-497D-A593-69BAEFB5106F} diff --git a/src/Plugins/BotSharp.Plugin.MiniMaxAI/BotSharp.Plugin.MiniMaxAI.csproj b/src/Plugins/BotSharp.Plugin.MiniMaxAI/BotSharp.Plugin.MiniMaxAI.csproj new file mode 100644 index 000000000..4f3e17b8a --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.MiniMaxAI/BotSharp.Plugin.MiniMaxAI.csproj @@ -0,0 +1,17 @@ + + + + $(TargetFramework) + enable + $(LangVersion) + $(BotSharpVersion) + $(GeneratePackageOnBuild) + $(GenerateDocumentationFile) + $(SolutionDir)packages + + + + + + + diff --git a/src/Plugins/BotSharp.Plugin.MiniMaxAI/MiniMaxAiPlugin.cs b/src/Plugins/BotSharp.Plugin.MiniMaxAI/MiniMaxAiPlugin.cs new file mode 100644 index 000000000..0cc18ceb3 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.MiniMaxAI/MiniMaxAiPlugin.cs @@ -0,0 +1,18 @@ +using BotSharp.Plugin.MiniMaxAI.Providers.Chat; +using BotSharp.Plugin.MiniMaxAI.Providers.Text; + +namespace BotSharp.Plugin.MiniMaxAI; + +public class MiniMaxAiPlugin : IBotSharpPlugin +{ + public string Id => "8a4ebd68-4d7d-4c5c-aed9-263946cc3a0d"; + public string Name => "MiniMax"; + public string Description => "MiniMax AI models exposed through the OpenAI-compatible API."; + public string IconUrl => "https://www.minimax.io/favicon.ico"; + + public void RegisterDI(IServiceCollection services, IConfiguration config) + { + services.AddScoped(); + services.AddScoped(); + } +} diff --git a/src/Plugins/BotSharp.Plugin.MiniMaxAI/Providers/Chat/ChatCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.MiniMaxAI/Providers/Chat/ChatCompletionProvider.cs new file mode 100644 index 000000000..3dc603b77 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.MiniMaxAI/Providers/Chat/ChatCompletionProvider.cs @@ -0,0 +1,15 @@ +namespace BotSharp.Plugin.MiniMaxAI.Providers.Chat; + +public class ChatCompletionProvider : global::BotSharp.Plugin.OpenAI.Providers.Chat.ChatCompletionProvider +{ + public override string Provider => "minimax"; + + public ChatCompletionProvider( + global::BotSharp.Plugin.OpenAI.Settings.OpenAiSettings settings, + ILogger logger, + IServiceProvider services, + IConversationStateService state, + IFileStorageService fileStorage) : base(settings, logger, services, state, fileStorage) + { + } +} diff --git a/src/Plugins/BotSharp.Plugin.MiniMaxAI/Providers/Text/TextCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.MiniMaxAI/Providers/Text/TextCompletionProvider.cs new file mode 100644 index 000000000..1f94a1ba9 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.MiniMaxAI/Providers/Text/TextCompletionProvider.cs @@ -0,0 +1,13 @@ +namespace BotSharp.Plugin.MiniMaxAI.Providers.Text; + +public class TextCompletionProvider : global::BotSharp.Plugin.OpenAI.Providers.Text.TextCompletionProvider +{ + public override string Provider => "minimax"; + + public TextCompletionProvider( + global::BotSharp.Plugin.OpenAI.Settings.OpenAiSettings settings, + ILogger logger, + IServiceProvider services) : base(settings, logger, services) + { + } +} diff --git a/src/Plugins/BotSharp.Plugin.MiniMaxAI/Using.cs b/src/Plugins/BotSharp.Plugin.MiniMaxAI/Using.cs new file mode 100644 index 000000000..06ffb04fd --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.MiniMaxAI/Using.cs @@ -0,0 +1,8 @@ +global using Microsoft.Extensions.Configuration; +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.Logging; + +global using BotSharp.Abstraction.Conversations; +global using BotSharp.Abstraction.Files; +global using BotSharp.Abstraction.MLTasks; +global using BotSharp.Abstraction.Plugins; diff --git a/src/WebStarter/WebStarter.csproj b/src/WebStarter/WebStarter.csproj index 11cba27bc..6f7129e45 100644 --- a/src/WebStarter/WebStarter.csproj +++ b/src/WebStarter/WebStarter.csproj @@ -62,6 +62,7 @@ + diff --git a/src/WebStarter/appsettings.json b/src/WebStarter/appsettings.json index 7688d81ea..79bbe9521 100644 --- a/src/WebStarter/appsettings.json +++ b/src/WebStarter/appsettings.json @@ -594,6 +594,30 @@ } ] }, + { + "Provider": "minimax", + "Models": [ + { + "Id": "MiniMax-M3", + "Name": "MiniMax-M3", + "Version": "MiniMax-M3", + "ApiKey": "", + "Endpoint": "https://api.minimax.io/v1", + "Type": "chat", + "Capabilities": [ + "Chat" + ], + "Cost": { + "TextInputCost": 0.0006, + "CachedTextInputCost": 0.00012, + "AudioInputCost": 0, + "CachedAudioInputCost": 0, + "TextOutputCost": 0.0024, + "AudioOutputCost": 0 + } + } + ] + }, { "Provider": "google-ai", "Models": [ @@ -1056,6 +1080,7 @@ "BotSharp.Plugin.GoogleAI", "BotSharp.Plugin.MetaAI", "BotSharp.Plugin.DeepSeekAI", + "BotSharp.Plugin.MiniMaxAI", "BotSharp.Plugin.MetaMessenger", "BotSharp.Plugin.HuggingFace", "BotSharp.Plugin.KnowledgeBase", From 895e058eaf98a7ff189ec7522a8ba7ad2e70f7f3 Mon Sep 17 00:00:00 2001 From: octo-patch <266937838+octo-patch@users.noreply.github.com> Date: Mon, 13 Jul 2026 17:23:55 +0800 Subject: [PATCH 2/2] Complete MiniMax provider coverage --- .../Conversations/Models/TokenStatsModel.cs | 5 +- .../MLTasks/Settings/LlmModelSetting.cs | 30 +- .../Conversations/Services/TokenStatistics.cs | 14 +- .../Providers/ChatCompletionProvider.cs | 38 +- .../Providers/ProviderHelper.cs | 4 + .../BotSharp.Plugin.AnthropicAI/Using.cs | 1 + .../BotSharp.Plugin.MiniMaxAI.csproj | 1 + .../MiniMaxAiPlugin.cs | 13 +- .../Chat/AnthropicChatCompletionProvider.cs | 13 + .../Chat/AnthropicCnChatCompletionProvider.cs | 13 + .../Providers/Chat/ChatCompletionProvider.cs | 36 +- .../Chat/OpenAiCnChatCompletionProvider.cs | 14 + .../Providers/Text/TextCompletionProvider.cs | 13 - .../BotSharp.Plugin.MiniMaxAI/Using.cs | 6 + .../Chat/ChatCompletionProvider.Chat.cs | 7 + .../Providers/Chat/ChatCompletionProvider.cs | 17 +- src/WebStarter/appsettings.json | 444 +++++++++++++++++- .../BotSharp.LLM.Tests.csproj | 4 + .../MiniMaxProviderTests.cs | 129 +++++ 19 files changed, 763 insertions(+), 39 deletions(-) create mode 100644 src/Plugins/BotSharp.Plugin.MiniMaxAI/Providers/Chat/AnthropicChatCompletionProvider.cs create mode 100644 src/Plugins/BotSharp.Plugin.MiniMaxAI/Providers/Chat/AnthropicCnChatCompletionProvider.cs create mode 100644 src/Plugins/BotSharp.Plugin.MiniMaxAI/Providers/Chat/OpenAiCnChatCompletionProvider.cs delete mode 100644 src/Plugins/BotSharp.Plugin.MiniMaxAI/Providers/Text/TextCompletionProvider.cs create mode 100644 tests/BotSharp.LLM.Tests/MiniMaxProviderTests.cs diff --git a/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/TokenStatsModel.cs b/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/TokenStatsModel.cs index c87564003..236c3b18c 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/TokenStatsModel.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/TokenStatsModel.cs @@ -5,10 +5,12 @@ public class TokenStatsModel public string Provider { get; set; } public string Model { get; set; } public string Prompt { get; set; } + public string? ServiceTier { get; set; } #region Text token public long TextInputTokens { get; set; } public long CachedTextInputTokens { get; set; } + public long CachedTextInputWriteTokens { get; set; } public long TextOutputTokens { get; set; } #endregion @@ -29,7 +31,8 @@ public class TokenStatsModel public float ImageGenerationUnitCost { get; set; } #endregion - public long TotalInputTokens => TextInputTokens + CachedTextInputTokens + public long TotalTextInputTokens => TextInputTokens + CachedTextInputTokens + CachedTextInputWriteTokens; + public long TotalInputTokens => TotalTextInputTokens + AudioInputTokens + CachedAudioInputTokens + ImageInputTokens + CachedImageInputTokens; public long TotalOutputTokens => TextOutputTokens + AudioOutputTokens + ImageOutputTokens; diff --git a/src/Infrastructure/BotSharp.Abstraction/MLTasks/Settings/LlmModelSetting.cs b/src/Infrastructure/BotSharp.Abstraction/MLTasks/Settings/LlmModelSetting.cs index 3915f9f6e..c843c0395 100644 --- a/src/Infrastructure/BotSharp.Abstraction/MLTasks/Settings/LlmModelSetting.cs +++ b/src/Infrastructure/BotSharp.Abstraction/MLTasks/Settings/LlmModelSetting.cs @@ -30,8 +30,10 @@ public class LlmModelSetting public string ApiKey { get; set; } = null!; public string? Endpoint { get; set; } + public long? ContextWindow { get; set; } public LlmModelType Type { get; set; } = LlmModelType.Chat; public List Capabilities { get; set; } = []; + public List InputModalities { get; set; } = []; /// /// If true, allow sending images/videos to this model @@ -183,7 +185,10 @@ public class LlmCostSetting #region Text token public float TextInputCost { get; set; } = 0f; public float CachedTextInputCost { get; set; } = 0f; + public float CachedTextInputWriteCost { get; set; } = 0f; public float TextOutputCost { get; set; } = 0f; + public string DefaultServiceTier { get; set; } = "standard"; + public IList? TextTokenCostTiers { get; set; } #endregion #region Audio token @@ -201,6 +206,29 @@ public class LlmCostSetting #region Image public IList? ImageCosts { get; set; } #endregion + + public LlmTextTokenCostTier? GetTextTokenCostTier(long inputTokens, string? serviceTier = null) + { + var selectedServiceTier = string.IsNullOrWhiteSpace(serviceTier) + ? DefaultServiceTier + : serviceTier; + + return TextTokenCostTiers?.FirstOrDefault(x => + string.Equals(x.ServiceTier, selectedServiceTier, StringComparison.OrdinalIgnoreCase) + && (!x.InputTokensGreaterThan.HasValue || inputTokens > x.InputTokensGreaterThan.Value) + && (!x.InputTokensLessThanOrEqual.HasValue || inputTokens <= x.InputTokensLessThanOrEqual.Value)); + } +} + +public class LlmTextTokenCostTier +{ + public string ServiceTier { get; set; } = "standard"; + public long? InputTokensGreaterThan { get; set; } + public long? InputTokensLessThanOrEqual { get; set; } + public float TextInputCost { get; set; } + public float CachedTextInputCost { get; set; } + public float CachedTextInputWriteCost { get; set; } + public float TextOutputCost { get; set; } } public class LlmImageCost @@ -240,4 +268,4 @@ public enum LlmModelCapability Realtime = 11, WebSearch = 12, PdfReading = 13 -} \ No newline at end of file +} diff --git a/src/Infrastructure/BotSharp.Core/Conversations/Services/TokenStatistics.cs b/src/Infrastructure/BotSharp.Core/Conversations/Services/TokenStatistics.cs index 542d6c008..7385ea773 100644 --- a/src/Infrastructure/BotSharp.Core/Conversations/Services/TokenStatistics.cs +++ b/src/Infrastructure/BotSharp.Core/Conversations/Services/TokenStatistics.cs @@ -41,9 +41,15 @@ public async Task AddToken(TokenStatsModel stats, RoleDialogModel message) var settings = settingsService.GetSetting(stats.Provider, _model); #region Text tokens - var deltaTextInputCost = GetDeltaCost(stats.TextInputTokens, settings?.Cost?.TextInputCost); - var deltaCachedTextInputCost = GetDeltaCost(stats.CachedTextInputTokens, settings?.Cost?.CachedTextInputCost); - var deltaTextOutputCost = GetDeltaCost(stats.TextOutputTokens, settings?.Cost?.TextOutputCost); + var textCostTier = settings?.Cost?.GetTextTokenCostTier(stats.TotalTextInputTokens, stats.ServiceTier); + var deltaTextInputCost = GetDeltaCost(stats.TextInputTokens, + textCostTier?.TextInputCost ?? settings?.Cost?.TextInputCost); + var deltaCachedTextInputCost = GetDeltaCost(stats.CachedTextInputTokens, + textCostTier?.CachedTextInputCost ?? settings?.Cost?.CachedTextInputCost); + var deltaCachedTextInputWriteCost = GetDeltaCost(stats.CachedTextInputWriteTokens, + textCostTier?.CachedTextInputWriteCost ?? settings?.Cost?.CachedTextInputWriteCost); + var deltaTextOutputCost = GetDeltaCost(stats.TextOutputTokens, + textCostTier?.TextOutputCost ?? settings?.Cost?.TextOutputCost); #endregion #region Audio tokens @@ -63,7 +69,7 @@ public async Task AddToken(TokenStatsModel stats, RoleDialogModel message) #endregion - var deltaPromptCost = deltaTextInputCost + deltaCachedTextInputCost + var deltaPromptCost = deltaTextInputCost + deltaCachedTextInputCost + deltaCachedTextInputWriteCost + deltaAudioInputCost + deltaCachedAudioInputCost + deltaImageInputCost + deltaCachedImageInputCost; var deltaCompletionCost = deltaTextOutputCost + deltaAudioOutputCost + deltaImageOutputCost; diff --git a/src/Plugins/BotSharp.Plugin.AnthropicAI/Providers/ChatCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.AnthropicAI/Providers/ChatCompletionProvider.cs index 8306c71be..fcb4d4b07 100644 --- a/src/Plugins/BotSharp.Plugin.AnthropicAI/Providers/ChatCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.AnthropicAI/Providers/ChatCompletionProvider.cs @@ -8,7 +8,7 @@ namespace BotSharp.Plugin.AnthropicAI.Providers; public class ChatCompletionProvider : IChatCompletion { - public string Provider => "anthropic"; + public virtual string Provider => "anthropic"; public string Model => _model; protected readonly AnthropicSettings _settings; @@ -94,7 +94,10 @@ public async Task GetChatCompletions(Agent agent, List GetChatCompletionsAsync(Agent agent, List GetChatCompletionsStreamingAsync(Agent agent, Prompt = prompt, Provider = Provider, Model = _model, + ServiceTier = tokenUsage?.ServiceTier.ToString(), TextInputTokens = tokenUsage?.InputTokens ?? 0, + CachedTextInputTokens = tokenUsage?.CacheReadInputTokens ?? 0, + CachedTextInputWriteTokens = tokenUsage?.CacheCreationInputTokens ?? 0, TextOutputTokens = tokenUsage?.OutputTokens ?? 0 }); } @@ -465,8 +474,25 @@ public void SetModelName(string model) return null; } - var thinking = new ThinkingParameters(); var param = settings.Reasoning.Parameters!; + var thinkingType = LlmUtility.GetModelParameter( + param, + "ThinkingType", + _state.GetState("thinking_type")); + if (thinkingType.IsEqualTo("disabled")) + { + return null; + } + + var thinking = new ThinkingParameters(); + if (thinkingType.IsEqualTo("adaptive")) + { + thinking.Type = ThinkingType.adaptive; + } + else if (thinkingType.IsEqualTo("enabled")) + { + thinking.Type = ThinkingType.enabled; + } var bt = _state.GetState("budget_tokens"); if (int.TryParse(bt, out var budgetTokens) @@ -484,7 +510,11 @@ public void SetModelName(string model) thinking.UseInterleavedThinking = useInterleavedThinking; } - return thinking.BudgetTokens > 0 ? thinking : null; + return thinkingType.IsEqualTo("adaptive") + || thinkingType.IsEqualTo("enabled") + || thinking.BudgetTokens > 0 + ? thinking + : null; } private string GetPrompt(MessageParameters parameters) @@ -576,4 +606,4 @@ private void CollectMessageContentParts(List contentParts, List(); var settings = settingsService.GetSetting(provider, model); var client = new AnthropicClient(new APIAuthentication(settings.ApiKey)); + if (!string.IsNullOrWhiteSpace(settings.Endpoint)) + { + client.ApiUrlFormat = $"{settings.Endpoint.TrimEnd('/')}/{{0}}/{{1}}"; + } return client; } } diff --git a/src/Plugins/BotSharp.Plugin.AnthropicAI/Using.cs b/src/Plugins/BotSharp.Plugin.AnthropicAI/Using.cs index 04f851eb0..7b87d4c24 100644 --- a/src/Plugins/BotSharp.Plugin.AnthropicAI/Using.cs +++ b/src/Plugins/BotSharp.Plugin.AnthropicAI/Using.cs @@ -26,6 +26,7 @@ global using BotSharp.Abstraction.Hooks; global using BotSharp.Abstraction.MessageHub.Models; global using BotSharp.Abstraction.MLTasks.Settings; +global using BotSharp.Abstraction.MLTasks.Utilities; global using BotSharp.Abstraction.Options; global using BotSharp.Abstraction.Utilities; diff --git a/src/Plugins/BotSharp.Plugin.MiniMaxAI/BotSharp.Plugin.MiniMaxAI.csproj b/src/Plugins/BotSharp.Plugin.MiniMaxAI/BotSharp.Plugin.MiniMaxAI.csproj index 4f3e17b8a..1ddba5cab 100644 --- a/src/Plugins/BotSharp.Plugin.MiniMaxAI/BotSharp.Plugin.MiniMaxAI.csproj +++ b/src/Plugins/BotSharp.Plugin.MiniMaxAI/BotSharp.Plugin.MiniMaxAI.csproj @@ -11,6 +11,7 @@ + diff --git a/src/Plugins/BotSharp.Plugin.MiniMaxAI/MiniMaxAiPlugin.cs b/src/Plugins/BotSharp.Plugin.MiniMaxAI/MiniMaxAiPlugin.cs index 0cc18ceb3..14fae8215 100644 --- a/src/Plugins/BotSharp.Plugin.MiniMaxAI/MiniMaxAiPlugin.cs +++ b/src/Plugins/BotSharp.Plugin.MiniMaxAI/MiniMaxAiPlugin.cs @@ -1,8 +1,11 @@ -using BotSharp.Plugin.MiniMaxAI.Providers.Chat; -using BotSharp.Plugin.MiniMaxAI.Providers.Text; +using AnthropicChatCompletionProvider = BotSharp.Plugin.MiniMaxAI.Providers.Chat.AnthropicChatCompletionProvider; +using AnthropicCnChatCompletionProvider = BotSharp.Plugin.MiniMaxAI.Providers.Chat.AnthropicCnChatCompletionProvider; +using OpenAiChatCompletionProvider = BotSharp.Plugin.MiniMaxAI.Providers.Chat.ChatCompletionProvider; +using OpenAiCnChatCompletionProvider = BotSharp.Plugin.MiniMaxAI.Providers.Chat.OpenAiCnChatCompletionProvider; namespace BotSharp.Plugin.MiniMaxAI; +[PluginDependency("BotSharp.Plugin.OpenAI", "BotSharp.Plugin.AnthropicAI")] public class MiniMaxAiPlugin : IBotSharpPlugin { public string Id => "8a4ebd68-4d7d-4c5c-aed9-263946cc3a0d"; @@ -12,7 +15,9 @@ public class MiniMaxAiPlugin : IBotSharpPlugin public void RegisterDI(IServiceCollection services, IConfiguration config) { - services.AddScoped(); - services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); } } diff --git a/src/Plugins/BotSharp.Plugin.MiniMaxAI/Providers/Chat/AnthropicChatCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.MiniMaxAI/Providers/Chat/AnthropicChatCompletionProvider.cs new file mode 100644 index 000000000..9e86b0c57 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.MiniMaxAI/Providers/Chat/AnthropicChatCompletionProvider.cs @@ -0,0 +1,13 @@ +namespace BotSharp.Plugin.MiniMaxAI.Providers.Chat; + +public class AnthropicChatCompletionProvider : global::BotSharp.Plugin.AnthropicAI.Providers.ChatCompletionProvider +{ + public override string Provider => "minimax-anthropic"; + + public AnthropicChatCompletionProvider( + IServiceProvider services, + ILogger logger, + IConversationStateService state) : base(new(), services, logger, state) + { + } +} diff --git a/src/Plugins/BotSharp.Plugin.MiniMaxAI/Providers/Chat/AnthropicCnChatCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.MiniMaxAI/Providers/Chat/AnthropicCnChatCompletionProvider.cs new file mode 100644 index 000000000..df9dfda45 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.MiniMaxAI/Providers/Chat/AnthropicCnChatCompletionProvider.cs @@ -0,0 +1,13 @@ +namespace BotSharp.Plugin.MiniMaxAI.Providers.Chat; + +public class AnthropicCnChatCompletionProvider : AnthropicChatCompletionProvider +{ + public override string Provider => "minimax-anthropic-cn"; + + public AnthropicCnChatCompletionProvider( + IServiceProvider services, + ILogger logger, + IConversationStateService state) : base(services, logger, state) + { + } +} diff --git a/src/Plugins/BotSharp.Plugin.MiniMaxAI/Providers/Chat/ChatCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.MiniMaxAI/Providers/Chat/ChatCompletionProvider.cs index 3dc603b77..41bb743fc 100644 --- a/src/Plugins/BotSharp.Plugin.MiniMaxAI/Providers/Chat/ChatCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.MiniMaxAI/Providers/Chat/ChatCompletionProvider.cs @@ -1,15 +1,47 @@ +using OpenAI.Chat; + namespace BotSharp.Plugin.MiniMaxAI.Providers.Chat; +#pragma warning disable SCME0001 + public class ChatCompletionProvider : global::BotSharp.Plugin.OpenAI.Providers.Chat.ChatCompletionProvider { public override string Provider => "minimax"; + protected override bool UseResponseApi => false; public ChatCompletionProvider( - global::BotSharp.Plugin.OpenAI.Settings.OpenAiSettings settings, ILogger logger, IServiceProvider services, IConversationStateService state, - IFileStorageService fileStorage) : base(settings, logger, services, state, fileStorage) + IFileStorageService fileStorage) : base(new(), logger, services, state, fileStorage) + { + } + + protected override void ConfigureChatCompletionOptions( + ChatCompletionOptions options, + LlmModelSetting? settings) { + var parameters = settings?.Reasoning?.Parameters; + var thinkingType = LlmUtility.GetModelParameter( + parameters, + "ThinkingType", + _state.GetState("thinking_type")); + if (thinkingType is "adaptive" or "disabled") + { + var thinking = JsonSerializer.SerializeToUtf8Bytes(new { type = thinkingType }); + options.Patch.Set("$.thinking"u8, BinaryData.FromBytes(thinking)); + } + + var serviceTiers = settings?.Cost?.TextTokenCostTiers? + .Select(x => x.ServiceTier) + .Distinct(StringComparer.OrdinalIgnoreCase); + var serviceTier = LlmUtility.VerifyModelParameter( + _state.GetState("service_tier"), + settings?.Cost?.DefaultServiceTier, + serviceTiers); + if (!string.IsNullOrWhiteSpace(serviceTier)) + { + options.Patch.Set("$.service_tier"u8, serviceTier); + } } } diff --git a/src/Plugins/BotSharp.Plugin.MiniMaxAI/Providers/Chat/OpenAiCnChatCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.MiniMaxAI/Providers/Chat/OpenAiCnChatCompletionProvider.cs new file mode 100644 index 000000000..ad49467af --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.MiniMaxAI/Providers/Chat/OpenAiCnChatCompletionProvider.cs @@ -0,0 +1,14 @@ +namespace BotSharp.Plugin.MiniMaxAI.Providers.Chat; + +public class OpenAiCnChatCompletionProvider : ChatCompletionProvider +{ + public override string Provider => "minimax-cn"; + + public OpenAiCnChatCompletionProvider( + ILogger logger, + IServiceProvider services, + IConversationStateService state, + IFileStorageService fileStorage) : base(logger, services, state, fileStorage) + { + } +} diff --git a/src/Plugins/BotSharp.Plugin.MiniMaxAI/Providers/Text/TextCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.MiniMaxAI/Providers/Text/TextCompletionProvider.cs deleted file mode 100644 index 1f94a1ba9..000000000 --- a/src/Plugins/BotSharp.Plugin.MiniMaxAI/Providers/Text/TextCompletionProvider.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace BotSharp.Plugin.MiniMaxAI.Providers.Text; - -public class TextCompletionProvider : global::BotSharp.Plugin.OpenAI.Providers.Text.TextCompletionProvider -{ - public override string Provider => "minimax"; - - public TextCompletionProvider( - global::BotSharp.Plugin.OpenAI.Settings.OpenAiSettings settings, - ILogger logger, - IServiceProvider services) : base(settings, logger, services) - { - } -} diff --git a/src/Plugins/BotSharp.Plugin.MiniMaxAI/Using.cs b/src/Plugins/BotSharp.Plugin.MiniMaxAI/Using.cs index 06ffb04fd..793ccd9d0 100644 --- a/src/Plugins/BotSharp.Plugin.MiniMaxAI/Using.cs +++ b/src/Plugins/BotSharp.Plugin.MiniMaxAI/Using.cs @@ -1,3 +1,7 @@ +global using System; +global using System.Linq; +global using System.Text.Json; + global using Microsoft.Extensions.Configuration; global using Microsoft.Extensions.DependencyInjection; global using Microsoft.Extensions.Logging; @@ -5,4 +9,6 @@ global using BotSharp.Abstraction.Conversations; global using BotSharp.Abstraction.Files; global using BotSharp.Abstraction.MLTasks; +global using BotSharp.Abstraction.MLTasks.Settings; +global using BotSharp.Abstraction.MLTasks.Utilities; global using BotSharp.Abstraction.Plugins; diff --git a/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Chat/ChatCompletionProvider.Chat.cs b/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Chat/ChatCompletionProvider.Chat.cs index 55dc8eeb6..85f804b7a 100644 --- a/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Chat/ChatCompletionProvider.Chat.cs +++ b/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Chat/ChatCompletionProvider.Chat.cs @@ -88,6 +88,7 @@ private async Task InnerGetChatCompletions(Agent agent, List InnerGetChatCompletionsAsync(Agent agent, Prompt = prompt, Provider = Provider, Model = _model, + ServiceTier = value.ServiceTier?.ToString(), TextInputTokens = (tokenUsage?.InputTokenCount ?? 0) - (inputTokenDetails?.CachedTokenCount ?? 0), CachedTextInputTokens = inputTokenDetails?.CachedTokenCount ?? 0, TextOutputTokens = tokenUsage?.OutputTokenCount ?? 0 @@ -231,6 +233,7 @@ private async Task InnerGetChatCompletionsStreamingAsync(Agent using var textStream = new RealtimeTextStream(); var toolCalls = new List(); ChatTokenUsage? tokenUsage = null; + string? serviceTier = null; var responseMessage = new RoleDialogModel(AgentRole.Assistant, string.Empty) { @@ -246,6 +249,7 @@ private async Task InnerGetChatCompletionsStreamingAsync(Agent await foreach (var choice in chatClient.CompleteChatStreamingAsync(messages, options, cancellationToken)) { tokenUsage = choice.Usage; + serviceTier = choice.ServiceTier?.ToString() ?? serviceTier; if (!choice.ToolCallUpdates.IsNullOrEmpty()) { @@ -355,6 +359,7 @@ private async Task InnerGetChatCompletionsStreamingAsync(Agent Prompt = prompt, Provider = Provider, Model = _model, + ServiceTier = serviceTier, TextInputTokens = (tokenUsage?.InputTokenCount ?? 0) - (inputTokenDetails?.CachedTokenCount ?? 0), CachedTextInputTokens = inputTokenDetails?.CachedTokenCount ?? 0, TextOutputTokens = tokenUsage?.OutputTokenCount ?? 0 @@ -626,6 +631,8 @@ private ChatCompletionOptions InitChatCompletionOption(Agent agent) WebSearchOptions = webSearchOptions }; + ConfigureChatCompletionOptions(options, settings); + if (webSearchOptions == null) { var format = _state.GetState("response_format").IfNullOrEmptyAs(agent.LlmConfig?.ResponseFormat); diff --git a/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Chat/ChatCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Chat/ChatCompletionProvider.cs index 98c89980b..1be73f23f 100644 --- a/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Chat/ChatCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Chat/ChatCompletionProvider.cs @@ -1,3 +1,5 @@ +using OpenAI.Chat; + namespace BotSharp.Plugin.OpenAI.Providers.Chat; public partial class ChatCompletionProvider : IChatCompletion @@ -15,6 +17,7 @@ public partial class ChatCompletionProvider : IChatCompletion public virtual string Provider => "openai"; public string Model => _model; + protected virtual bool UseResponseApi => _settings.UseResponseApi; public ChatCompletionProvider( OpenAiSettings settings, @@ -33,7 +36,7 @@ public ChatCompletionProvider( public async Task GetChatCompletions(Agent agent, List conversations) { - if (_settings.UseResponseApi) + if (UseResponseApi) { return await InnerCreateResponse(agent, conversations); } @@ -48,7 +51,7 @@ public async Task GetChatCompletionsAsync(Agent agent, Func onMessageReceived, Func onFunctionExecuting) { - if (_settings.UseResponseApi) + if (UseResponseApi) { return await InnerCreateResponseAsync(agent, conversations, onMessageReceived, onFunctionExecuting); } @@ -60,7 +63,7 @@ public async Task GetChatCompletionsAsync(Agent agent, public async Task GetChatCompletionsStreamingAsync(Agent agent, List conversations) { - if (_settings.UseResponseApi) + if (UseResponseApi) { return await InnerCreateResponseStreamingAsync(agent, conversations); } @@ -81,9 +84,15 @@ public void SetApiKey(string apiKey) _apiKey = apiKey; } + protected virtual void ConfigureChatCompletionOptions( + ChatCompletionOptions options, + LlmModelSetting? settings) + { + } + private static bool IsImageContentType(string? contentType) { return !string.IsNullOrEmpty(contentType) && contentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase); } -} \ No newline at end of file +} diff --git a/src/WebStarter/appsettings.json b/src/WebStarter/appsettings.json index 79bbe9521..34dd1afb8 100644 --- a/src/WebStarter/appsettings.json +++ b/src/WebStarter/appsettings.json @@ -603,17 +603,449 @@ "Version": "MiniMax-M3", "ApiKey": "", "Endpoint": "https://api.minimax.io/v1", + "ContextWindow": 1000000, + "Type": "chat", + "MultiModal": true, + "Capabilities": [ + "Chat", + "ImageReading" + ], + "InputModalities": [ + "text", + "image", + "video" + ], + "Reasoning": { + "Parameters": { + "ThinkingType": { + "Default": "adaptive", + "Options": [ "adaptive", "disabled" ] + } + } + }, + "Cost": { + "TextInputCost": 0.0003, + "CachedTextInputCost": 0.00006, + "TextOutputCost": 0.0012, + "DefaultServiceTier": "standard", + "TextTokenCostTiers": [ + { + "ServiceTier": "standard", + "InputTokensLessThanOrEqual": 512000, + "TextInputCost": 0.0003, + "CachedTextInputCost": 0.00006, + "TextOutputCost": 0.0012 + }, + { + "ServiceTier": "standard", + "InputTokensGreaterThan": 512000, + "TextInputCost": 0.0006, + "CachedTextInputCost": 0.00012, + "TextOutputCost": 0.0024 + }, + { + "ServiceTier": "priority", + "InputTokensLessThanOrEqual": 512000, + "TextInputCost": 0.00045, + "CachedTextInputCost": 0.00009, + "TextOutputCost": 0.0018 + }, + { + "ServiceTier": "priority", + "InputTokensGreaterThan": 512000, + "TextInputCost": 0.0009, + "CachedTextInputCost": 0.00018, + "TextOutputCost": 0.0036 + } + ] + } + }, + { + "Id": "MiniMax-M2.7", + "Name": "MiniMax-M2.7", + "Version": "MiniMax-M2.7", + "ApiKey": "", + "Endpoint": "https://api.minimax.io/v1", + "ContextWindow": 204800, "Type": "chat", "Capabilities": [ "Chat" ], + "InputModalities": [ + "text" + ], + "Reasoning": { + "Parameters": { + "ThinkingType": { + "Default": "always_on", + "Options": [ "always_on" ] + } + } + }, "Cost": { - "TextInputCost": 0.0006, - "CachedTextInputCost": 0.00012, - "AudioInputCost": 0, - "CachedAudioInputCost": 0, - "TextOutputCost": 0.0024, - "AudioOutputCost": 0 + "TextInputCost": 0.0003, + "CachedTextInputCost": 0.00006, + "CachedTextInputWriteCost": 0.000375, + "TextOutputCost": 0.0012, + "DefaultServiceTier": "standard", + "TextTokenCostTiers": [ + { + "ServiceTier": "standard", + "TextInputCost": 0.0003, + "CachedTextInputCost": 0.00006, + "CachedTextInputWriteCost": 0.000375, + "TextOutputCost": 0.0012 + }, + { + "ServiceTier": "priority", + "TextInputCost": 0.00045, + "CachedTextInputCost": 0.00009, + "CachedTextInputWriteCost": 0.0005625, + "TextOutputCost": 0.0018 + } + ] + } + } + ] + }, + { + "Provider": "minimax-cn", + "Models": [ + { + "Id": "MiniMax-M3", + "Name": "MiniMax-M3", + "Version": "MiniMax-M3", + "ApiKey": "", + "Endpoint": "https://api.minimaxi.com/v1", + "ContextWindow": 1000000, + "Type": "chat", + "MultiModal": true, + "Capabilities": [ + "Chat", + "ImageReading" + ], + "InputModalities": [ + "text", + "image", + "video" + ], + "Reasoning": { + "Parameters": { + "ThinkingType": { + "Default": "adaptive", + "Options": [ "adaptive", "disabled" ] + } + } + }, + "Cost": { + "TextInputCost": 0.0003, + "CachedTextInputCost": 0.00006, + "TextOutputCost": 0.0012, + "DefaultServiceTier": "standard", + "TextTokenCostTiers": [ + { + "ServiceTier": "standard", + "InputTokensLessThanOrEqual": 512000, + "TextInputCost": 0.0003, + "CachedTextInputCost": 0.00006, + "TextOutputCost": 0.0012 + }, + { + "ServiceTier": "standard", + "InputTokensGreaterThan": 512000, + "TextInputCost": 0.0006, + "CachedTextInputCost": 0.00012, + "TextOutputCost": 0.0024 + }, + { + "ServiceTier": "priority", + "InputTokensLessThanOrEqual": 512000, + "TextInputCost": 0.00045, + "CachedTextInputCost": 0.00009, + "TextOutputCost": 0.0018 + }, + { + "ServiceTier": "priority", + "InputTokensGreaterThan": 512000, + "TextInputCost": 0.0009, + "CachedTextInputCost": 0.00018, + "TextOutputCost": 0.0036 + } + ] + } + }, + { + "Id": "MiniMax-M2.7", + "Name": "MiniMax-M2.7", + "Version": "MiniMax-M2.7", + "ApiKey": "", + "Endpoint": "https://api.minimaxi.com/v1", + "ContextWindow": 204800, + "Type": "chat", + "Capabilities": [ + "Chat" + ], + "InputModalities": [ + "text" + ], + "Reasoning": { + "Parameters": { + "ThinkingType": { + "Default": "always_on", + "Options": [ "always_on" ] + } + } + }, + "Cost": { + "TextInputCost": 0.0003, + "CachedTextInputCost": 0.00006, + "CachedTextInputWriteCost": 0.000375, + "TextOutputCost": 0.0012, + "DefaultServiceTier": "standard", + "TextTokenCostTiers": [ + { + "ServiceTier": "standard", + "TextInputCost": 0.0003, + "CachedTextInputCost": 0.00006, + "CachedTextInputWriteCost": 0.000375, + "TextOutputCost": 0.0012 + }, + { + "ServiceTier": "priority", + "TextInputCost": 0.00045, + "CachedTextInputCost": 0.00009, + "CachedTextInputWriteCost": 0.0005625, + "TextOutputCost": 0.0018 + } + ] + } + } + ] + }, + { + "Provider": "minimax-anthropic", + "Models": [ + { + "Id": "MiniMax-M3", + "Name": "MiniMax-M3", + "Version": "MiniMax-M3", + "ApiKey": "", + "Endpoint": "https://api.minimax.io/anthropic", + "ContextWindow": 1000000, + "Type": "chat", + "MultiModal": true, + "Capabilities": [ + "Chat", + "ImageReading" + ], + "InputModalities": [ + "text", + "image", + "video" + ], + "Reasoning": { + "Parameters": { + "ThinkingType": { + "Default": "disabled", + "Options": [ "adaptive", "disabled" ] + } + } + }, + "Cost": { + "TextInputCost": 0.0003, + "CachedTextInputCost": 0.00006, + "TextOutputCost": 0.0012, + "DefaultServiceTier": "standard", + "TextTokenCostTiers": [ + { + "ServiceTier": "standard", + "InputTokensLessThanOrEqual": 512000, + "TextInputCost": 0.0003, + "CachedTextInputCost": 0.00006, + "TextOutputCost": 0.0012 + }, + { + "ServiceTier": "standard", + "InputTokensGreaterThan": 512000, + "TextInputCost": 0.0006, + "CachedTextInputCost": 0.00012, + "TextOutputCost": 0.0024 + }, + { + "ServiceTier": "priority", + "InputTokensLessThanOrEqual": 512000, + "TextInputCost": 0.00045, + "CachedTextInputCost": 0.00009, + "TextOutputCost": 0.0018 + }, + { + "ServiceTier": "priority", + "InputTokensGreaterThan": 512000, + "TextInputCost": 0.0009, + "CachedTextInputCost": 0.00018, + "TextOutputCost": 0.0036 + } + ] + } + }, + { + "Id": "MiniMax-M2.7", + "Name": "MiniMax-M2.7", + "Version": "MiniMax-M2.7", + "ApiKey": "", + "Endpoint": "https://api.minimax.io/anthropic", + "ContextWindow": 204800, + "Type": "chat", + "Capabilities": [ + "Chat" + ], + "InputModalities": [ + "text" + ], + "Reasoning": { + "Parameters": { + "ThinkingType": { + "Default": "always_on", + "Options": [ "always_on" ] + } + } + }, + "Cost": { + "TextInputCost": 0.0003, + "CachedTextInputCost": 0.00006, + "CachedTextInputWriteCost": 0.000375, + "TextOutputCost": 0.0012, + "DefaultServiceTier": "standard", + "TextTokenCostTiers": [ + { + "ServiceTier": "standard", + "TextInputCost": 0.0003, + "CachedTextInputCost": 0.00006, + "CachedTextInputWriteCost": 0.000375, + "TextOutputCost": 0.0012 + }, + { + "ServiceTier": "priority", + "TextInputCost": 0.00045, + "CachedTextInputCost": 0.00009, + "CachedTextInputWriteCost": 0.0005625, + "TextOutputCost": 0.0018 + } + ] + } + } + ] + }, + { + "Provider": "minimax-anthropic-cn", + "Models": [ + { + "Id": "MiniMax-M3", + "Name": "MiniMax-M3", + "Version": "MiniMax-M3", + "ApiKey": "", + "Endpoint": "https://api.minimaxi.com/anthropic", + "ContextWindow": 1000000, + "Type": "chat", + "MultiModal": true, + "Capabilities": [ + "Chat", + "ImageReading" + ], + "InputModalities": [ + "text", + "image", + "video" + ], + "Reasoning": { + "Parameters": { + "ThinkingType": { + "Default": "disabled", + "Options": [ "adaptive", "disabled" ] + } + } + }, + "Cost": { + "TextInputCost": 0.0003, + "CachedTextInputCost": 0.00006, + "TextOutputCost": 0.0012, + "DefaultServiceTier": "standard", + "TextTokenCostTiers": [ + { + "ServiceTier": "standard", + "InputTokensLessThanOrEqual": 512000, + "TextInputCost": 0.0003, + "CachedTextInputCost": 0.00006, + "TextOutputCost": 0.0012 + }, + { + "ServiceTier": "standard", + "InputTokensGreaterThan": 512000, + "TextInputCost": 0.0006, + "CachedTextInputCost": 0.00012, + "TextOutputCost": 0.0024 + }, + { + "ServiceTier": "priority", + "InputTokensLessThanOrEqual": 512000, + "TextInputCost": 0.00045, + "CachedTextInputCost": 0.00009, + "TextOutputCost": 0.0018 + }, + { + "ServiceTier": "priority", + "InputTokensGreaterThan": 512000, + "TextInputCost": 0.0009, + "CachedTextInputCost": 0.00018, + "TextOutputCost": 0.0036 + } + ] + } + }, + { + "Id": "MiniMax-M2.7", + "Name": "MiniMax-M2.7", + "Version": "MiniMax-M2.7", + "ApiKey": "", + "Endpoint": "https://api.minimaxi.com/anthropic", + "ContextWindow": 204800, + "Type": "chat", + "Capabilities": [ + "Chat" + ], + "InputModalities": [ + "text" + ], + "Reasoning": { + "Parameters": { + "ThinkingType": { + "Default": "always_on", + "Options": [ "always_on" ] + } + } + }, + "Cost": { + "TextInputCost": 0.0003, + "CachedTextInputCost": 0.00006, + "CachedTextInputWriteCost": 0.000375, + "TextOutputCost": 0.0012, + "DefaultServiceTier": "standard", + "TextTokenCostTiers": [ + { + "ServiceTier": "standard", + "TextInputCost": 0.0003, + "CachedTextInputCost": 0.00006, + "CachedTextInputWriteCost": 0.000375, + "TextOutputCost": 0.0012 + }, + { + "ServiceTier": "priority", + "TextInputCost": 0.00045, + "CachedTextInputCost": 0.00009, + "CachedTextInputWriteCost": 0.0005625, + "TextOutputCost": 0.0018 + } + ] } } ] diff --git a/tests/BotSharp.LLM.Tests/BotSharp.LLM.Tests.csproj b/tests/BotSharp.LLM.Tests/BotSharp.LLM.Tests.csproj index c95d7cfa2..0223aac31 100644 --- a/tests/BotSharp.LLM.Tests/BotSharp.LLM.Tests.csproj +++ b/tests/BotSharp.LLM.Tests/BotSharp.LLM.Tests.csproj @@ -24,6 +24,7 @@ + @@ -34,5 +35,8 @@ PreserveNewest + + PreserveNewest + diff --git a/tests/BotSharp.LLM.Tests/MiniMaxProviderTests.cs b/tests/BotSharp.LLM.Tests/MiniMaxProviderTests.cs new file mode 100644 index 000000000..716dfcb1c --- /dev/null +++ b/tests/BotSharp.LLM.Tests/MiniMaxProviderTests.cs @@ -0,0 +1,129 @@ +using BotSharp.Abstraction.MLTasks; +using BotSharp.Abstraction.MLTasks.Settings; +using BotSharp.Core.Infrastructures; +using BotSharp.Plugin.MiniMaxAI; +using BotSharp.Plugin.MiniMaxAI.Providers.Chat; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Shouldly; +using AnthropicProviderHelper = BotSharp.Plugin.AnthropicAI.Providers.ProviderHelper; + +namespace BotSharp.LLM.Tests; + +public class MiniMaxProviderTests +{ + private static readonly Dictionary ExpectedEndpoints = new() + { + ["minimax"] = "https://api.minimax.io/v1", + ["minimax-cn"] = "https://api.minimaxi.com/v1", + ["minimax-anthropic"] = "https://api.minimax.io/anthropic", + ["minimax-anthropic-cn"] = "https://api.minimaxi.com/anthropic" + }; + + [Fact] + public void RegistersEveryProtocolAndRegionAlias() + { + var services = new ServiceCollection(); + new MiniMaxAiPlugin().RegisterDI(services, new ConfigurationBuilder().Build()); + + var providerTypes = services + .Where(x => x.ServiceType == typeof(IChatCompletion)) + .Select(x => x.ImplementationType) + .ToArray(); + + providerTypes.ShouldBe([ + typeof(ChatCompletionProvider), + typeof(OpenAiCnChatCompletionProvider), + typeof(AnthropicChatCompletionProvider), + typeof(AnthropicCnChatCompletionProvider) + ]); + } + + [Fact] + public void WebStarterSettingsCoverModelsEndpointsAndCapabilities() + { + var configuration = new ConfigurationBuilder() + .SetBasePath(AppContext.BaseDirectory) + .AddJsonFile("WebStarter.appsettings.json") + .Build(); + var providers = configuration.GetSection("LlmProviders").Get>(); + + providers.ShouldNotBeNull(); + foreach (var expected in ExpectedEndpoints) + { + var provider = providers.Single(x => x.Provider == expected.Key); + provider.Models.Select(x => x.Id).ShouldBe(["MiniMax-M3", "MiniMax-M2.7"]); + provider.Models.ShouldAllBe(x => x.Endpoint == expected.Value); + + var m3 = provider.Models.Single(x => x.Id == "MiniMax-M3"); + m3.ContextWindow.ShouldBe(1_000_000); + m3.MultiModal.ShouldBeTrue(); + m3.InputModalities.ShouldBe(["text", "image", "video"]); + m3.Reasoning!.Parameters!["ThinkingType"].Default.ShouldBe( + expected.Key.Contains("anthropic", StringComparison.Ordinal) + ? "disabled" + : "adaptive"); + m3.Cost.TextTokenCostTiers!.Count.ShouldBe(4); + + var m27 = provider.Models.Single(x => x.Id == "MiniMax-M2.7"); + m27.ContextWindow.ShouldBe(204_800); + m27.MultiModal.ShouldBeFalse(); + m27.InputModalities.ShouldBe(["text"]); + m27.Reasoning!.Parameters!["ThinkingType"].Default.ShouldBe("always_on"); + m27.Cost.CachedTextInputWriteCost.ShouldBe(0.000375f); + } + } + + [Fact] + public void AnthropicClientUsesConfiguredBaseUrl() + { + const string providerName = "minimax-anthropic"; + const string modelName = "MiniMax-M3"; + var services = new ServiceCollection(); + services.AddLogging(); + services.AddSingleton(new List + { + new() + { + Provider = providerName, + Models = + [ + new() + { + Id = modelName, + Name = modelName, + ApiKey = "test-key", + Endpoint = "https://api.minimax.io/anthropic" + } + ] + } + }); + services.AddSingleton(); + + using var serviceProvider = services.BuildServiceProvider(); + using var client = AnthropicProviderHelper.GetAnthropicClient(providerName, modelName, serviceProvider); + + client.ApiUrlFormat.ShouldBe("https://api.minimax.io/anthropic/{0}/{1}"); + } + + [Fact] + public void SelectsTextCostTierByServiceTierAndInputLength() + { + var cost = new LlmCostSetting + { + DefaultServiceTier = "standard", + TextTokenCostTiers = + [ + new() { ServiceTier = "standard", InputTokensLessThanOrEqual = 512_000, TextInputCost = 0.0003f }, + new() { ServiceTier = "standard", InputTokensGreaterThan = 512_000, TextInputCost = 0.0006f }, + new() { ServiceTier = "priority", InputTokensLessThanOrEqual = 512_000, TextInputCost = 0.00045f }, + new() { ServiceTier = "priority", InputTokensGreaterThan = 512_000, TextInputCost = 0.0009f } + ] + }; + + cost.GetTextTokenCostTier(512_000)!.TextInputCost.ShouldBe(0.0003f); + cost.GetTextTokenCostTier(512_001)!.TextInputCost.ShouldBe(0.0006f); + cost.GetTextTokenCostTier(512_000, "priority")!.TextInputCost.ShouldBe(0.00045f); + cost.GetTextTokenCostTier(512_001, "priority")!.TextInputCost.ShouldBe(0.0009f); + } +}