From 788326381f4896db22722f0df13a546774b9a576 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sun, 16 Nov 2025 02:44:44 +0800 Subject: [PATCH] :sparkles: Multi model support --- .../Thought/ThoughtController.cs | 82 ++++++++-- .../Thought/ThoughtProvider.cs | 142 ++++++++++++------ DysonNetwork.Insight/appsettings.json | 23 ++- 3 files changed, 187 insertions(+), 60 deletions(-) diff --git a/DysonNetwork.Insight/Thought/ThoughtController.cs b/DysonNetwork.Insight/Thought/ThoughtController.cs index cb8a9da..ebf857e 100644 --- a/DysonNetwork.Insight/Thought/ThoughtController.cs +++ b/DysonNetwork.Insight/Thought/ThoughtController.cs @@ -2,6 +2,7 @@ using System.ComponentModel.DataAnnotations; using System.Diagnostics.CodeAnalysis; using System.Text; using System.Text.Json; +using DysonNetwork.Shared.Auth; using DysonNetwork.Shared.Models; using DysonNetwork.Shared.Proto; using Microsoft.AspNetCore.Mvc; @@ -19,12 +20,45 @@ public class ThoughtController(ThoughtProvider provider, ThoughtService service) public class StreamThinkingRequest { [Required] public string UserMessage { get; set; } = null!; + public string? ServiceId { get; set; } public Guid? SequenceId { get; set; } public List? AttachedPosts { get; set; } public List>? AttachedMessages { get; set; } public List AcceptProposals { get; set; } = []; } + public class ThoughtServiceInfo + { + public string ServiceId { get; set; } = null!; + public double BillingMultiplier { get; set; } + public int PerkLevel { get; set; } + } + + public class ThoughtServicesResponse + { + public string DefaultService { get; set; } = null!; + public IEnumerable Services { get; set; } = null!; + } + + [HttpGet("services")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult GetAvailableServices() + { + var services = provider.GetAvailableServicesInfo() + .Select(s => new ThoughtServiceInfo + { + ServiceId = s.ServiceId, + BillingMultiplier = s.BillingMultiplier, + PerkLevel = s.PerkLevel + }); + + return Ok(new ThoughtServicesResponse + { + DefaultService = provider.GetDefaultServiceId(), + Services = services + }); + } + [HttpPost] [Experimental("SKEXP0110")] public async Task Think([FromBody] StreamThinkingRequest request) @@ -35,6 +69,26 @@ public class ThoughtController(ThoughtProvider provider, ThoughtService service) if (request.AcceptProposals.Any(e => !AvailableProposals.Contains(e))) return BadRequest("Request contains unavailable proposal"); + var serviceId = provider.GetServiceId(request.ServiceId); + var serviceInfo = provider.GetServiceInfo(serviceId); + if (serviceInfo is null) + { + return BadRequest("Service not found or configured."); + } + + // TODO: Check perk level from `currentUser` + if (serviceInfo.PerkLevel > 0 && !currentUser.IsSuperuser) + if (currentUser.PerkSubscription is null || + PerkSubscriptionPrivilege.GetPrivilegeFromIdentifier(currentUser.PerkSubscription.Identifier) < + serviceInfo.PerkLevel) + return StatusCode(403, "Not enough perk level"); + + var kernel = provider.GetKernel(request.ServiceId); + if (kernel is null) + { + return BadRequest("Service not found or configured."); + } + // Generate a topic if creating a new sequence string? topic = null; if (!request.SequenceId.HasValue) @@ -46,7 +100,13 @@ public class ThoughtController(ThoughtProvider provider, ThoughtService service) ); summaryHistory.AddUserMessage(request.UserMessage); - var summaryResult = await provider.Kernel + var summaryKernel = provider.GetKernel(); // Get default kernel + if (summaryKernel is null) + { + return BadRequest("Default service not found or configured."); + } + + var summaryResult = await summaryKernel .GetRequiredService() .GetChatMessageContentAsync(summaryHistory); @@ -58,14 +118,13 @@ public class ThoughtController(ThoughtProvider provider, ThoughtService service) if (sequence == null) return Forbid(); // or NotFound // Save user thought - await service.SaveThoughtAsync(sequence, new List - { - new() + await service.SaveThoughtAsync(sequence, [ + new SnThinkingMessagePart { Type = ThinkingMessagePartType.Text, Text = request.UserMessage } - }, ThinkingThoughtRole.User); + ], ThinkingThoughtRole.User); // Build chat history var chatHistory = new ChatHistory( @@ -172,12 +231,10 @@ public class ThoughtController(ThoughtProvider provider, ThoughtService service) chatHistory.Add(assistantMessage); - if (functionResults.Count > 0) + if (functionResults.Count <= 0) continue; + foreach (var fr in functionResults) { - foreach (var fr in functionResults) - { - chatHistory.Add(fr.ToChatMessage()); - } + chatHistory.Add(fr.ToChatMessage()); } } } @@ -188,9 +245,8 @@ public class ThoughtController(ThoughtProvider provider, ThoughtService service) Response.Headers.Append("Content-Type", "text/event-stream"); Response.StatusCode = 200; - var kernel = provider.Kernel; var chatCompletionService = kernel.GetRequiredService(); - var executionSettings = provider.CreatePromptExecutionSettings(); + var executionSettings = provider.CreatePromptExecutionSettings(request.ServiceId); var assistantParts = new List(); @@ -300,7 +356,7 @@ public class ThoughtController(ThoughtProvider provider, ThoughtService service) sequence, assistantParts, ThinkingThoughtRole.Assistant, - provider.ModelDefault + serviceId ); // Write the topic if it was newly set, then the thought object as JSON to the stream diff --git a/DysonNetwork.Insight/Thought/ThoughtProvider.cs b/DysonNetwork.Insight/Thought/ThoughtProvider.cs index b35cd27..812a6da 100644 --- a/DysonNetwork.Insight/Thought/ThoughtProvider.cs +++ b/DysonNetwork.Insight/Thought/ThoughtProvider.cs @@ -13,6 +13,15 @@ using Microsoft.SemanticKernel.Plugins.Web.Google; namespace DysonNetwork.Insight.Thought; +public class ThoughtServiceModel +{ + public string ServiceId { get; set; } = null!; + public string? Provider { get; set; } + public string? Model { get; set; } + public double BillingMultiplier { get; set; } + public int PerkLevel { get; set; } +} + public class ThoughtProvider { private readonly PostService.PostServiceClient _postClient; @@ -20,11 +29,10 @@ public class ThoughtProvider private readonly IConfiguration _configuration; private readonly ILogger _logger; - public Kernel Kernel { get; } - - private string? ModelProviderType { get; set; } - public string? ModelDefault { get; set; } - public List ModelAvailable { get; set; } = []; + private readonly Dictionary _kernels = new(); + private readonly Dictionary _serviceProviders = new(); + private readonly Dictionary _serviceModels = new(); + private readonly string _defaultServiceId; [Experimental("SKEXP0050")] public ThoughtProvider( @@ -39,41 +47,59 @@ public class ThoughtProvider _accountClient = accountServiceClient; _configuration = configuration; - Kernel = InitializeThinkingProvider(configuration); - InitializeHelperFunctions(); + var cfg = configuration.GetSection("Thinking"); + _defaultServiceId = cfg.GetValue("DefaultService")!; + var services = cfg.GetSection("Services").GetChildren(); + + foreach (var service in services) + { + var serviceId = service.Key; + var serviceModel = new ThoughtServiceModel + { + ServiceId = serviceId, + Provider = service.GetValue("Provider"), + Model = service.GetValue("Model"), + BillingMultiplier = service.GetValue("BillingMultiplier", 1.0), + PerkLevel = service.GetValue("PerkLevel", 0) + }; + _serviceModels[serviceId] = serviceModel; + + var providerType = service.GetValue("Provider")?.ToLower(); + if (providerType is null) continue; + + var kernel = InitializeThinkingService(service); + InitializeHelperFunctions(kernel); + _kernels[serviceId] = kernel; + _serviceProviders[serviceId] = providerType; + } } - private Kernel InitializeThinkingProvider(IConfiguration configuration) + private Kernel InitializeThinkingService(IConfigurationSection serviceConfig) { - var cfg = configuration.GetSection("Thinking"); - ModelProviderType = cfg.GetValue("Provider")?.ToLower(); - ModelDefault = cfg.GetValue("Model"); - ModelAvailable = cfg.GetValue>("ModelAvailable") ?? []; - var endpoint = cfg.GetValue("Endpoint"); - var apiKey = cfg.GetValue("ApiKey"); + var providerType = serviceConfig.GetValue("Provider")?.ToLower(); + var model = serviceConfig.GetValue("Model"); + var endpoint = serviceConfig.GetValue("Endpoint"); + var apiKey = serviceConfig.GetValue("ApiKey"); var builder = Kernel.CreateBuilder(); - switch (ModelProviderType) + switch (providerType) { case "ollama": - foreach (var model in ModelAvailable) - builder.AddOllamaChatCompletion( - ModelDefault!, - new Uri(endpoint ?? "http://localhost:11434/api"), - model - ); + builder.AddOllamaChatCompletion( + model!, + new Uri(endpoint ?? "http://localhost:11434/api") + ); break; case "deepseek": var client = new OpenAIClient( new ApiKeyCredential(apiKey!), new OpenAIClientOptions { Endpoint = new Uri(endpoint ?? "https://api.deepseek.com/v1") } ); - foreach (var model in ModelAvailable) - builder.AddOpenAIChatCompletion(ModelDefault!, client, model); + builder.AddOpenAIChatCompletion(model!, client); break; default: - throw new IndexOutOfRangeException("Unknown thinking provider: " + ModelProviderType); + throw new IndexOutOfRangeException("Unknown thinking provider: " + providerType); } // Add gRPC clients for Thought Plugins @@ -89,7 +115,7 @@ public class ThoughtProvider } [Experimental("SKEXP0050")] - private void InitializeHelperFunctions() + private void InitializeHelperFunctions(Kernel kernel) { // Add web search plugins if configured var bingApiKey = _configuration.GetValue("Thinking:BingApiKey"); @@ -97,7 +123,7 @@ public class ThoughtProvider { var bingConnector = new BingConnector(bingApiKey); var bing = new WebSearchEnginePlugin(bingConnector); - Kernel.ImportPluginFromObject(bing, "bing"); + kernel.ImportPluginFromObject(bing, "bing"); } var googleApiKey = _configuration.GetValue("Thinking:GoogleApiKey"); @@ -108,26 +134,58 @@ public class ThoughtProvider apiKey: googleApiKey, searchEngineId: googleCx); var google = new WebSearchEnginePlugin(googleConnector); - Kernel.ImportPluginFromObject(google, "google"); + kernel.ImportPluginFromObject(google, "google"); } } - public PromptExecutionSettings CreatePromptExecutionSettings() + public Kernel? GetKernel(string? serviceId = null) { - switch (ModelProviderType) + serviceId ??= _defaultServiceId; + return _kernels.GetValueOrDefault(serviceId); + } + + public string GetServiceId(string? serviceId = null) + { + return serviceId ?? _defaultServiceId; + } + + public IEnumerable GetAvailableServices() + { + return _kernels.Keys; + } + + public IEnumerable GetAvailableServicesInfo() + { + return _serviceModels.Values; + } + + public ThoughtServiceModel? GetServiceInfo(string? serviceId) + { + serviceId ??= _defaultServiceId; + return _serviceModels.GetValueOrDefault(serviceId); + } + + public string GetDefaultServiceId() + { + return _defaultServiceId; + } + + public PromptExecutionSettings CreatePromptExecutionSettings(string? serviceId = null) + { + serviceId ??= _defaultServiceId; + var providerType = _serviceProviders.GetValueOrDefault(serviceId); + + return providerType switch { - case "ollama": - return new OllamaPromptExecutionSettings - { - FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(autoInvoke: false) - }; - case "deepseek": - return new OpenAIPromptExecutionSettings - { - FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(autoInvoke: false) - }; - default: - throw new InvalidOperationException("Unknown provider: " + ModelProviderType); - } + "ollama" => new OllamaPromptExecutionSettings + { + FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(autoInvoke: false) + }, + "deepseek" => new OpenAIPromptExecutionSettings + { + FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(autoInvoke: false), ModelId = serviceId + }, + _ => throw new InvalidOperationException("Unknown provider for service: " + serviceId) + }; } } \ No newline at end of file diff --git a/DysonNetwork.Insight/appsettings.json b/DysonNetwork.Insight/appsettings.json index a826620..207c7c3 100644 --- a/DysonNetwork.Insight/appsettings.json +++ b/DysonNetwork.Insight/appsettings.json @@ -20,9 +20,22 @@ "Insecure": true }, "Thinking": { - "Provider": "deepseek", - "Model": "deepseek-chat", - "ModelAvailable": ["deepseek-chat", "deepseek-reasoner"], - "ApiKey": "sk-bd20f6a2e9fa40b98c46899baa0e9f09" + "DefaultService": "deepseek-chat", + "Services": { + "deepseek-chat": { + "Provider": "deepseek", + "Model": "deepseek-chat", + "ApiKey": "sk-", + "BillingMultiplier": 1.0, + "PerkLevel": 0 + }, + "deepseek-reasoner": { + "Provider": "deepseek", + "Model": "deepseek-reasoner", + "ApiKey": "sk-", + "BillingMultiplier": 1.5, + "PerkLevel": 1 + } + } } -} \ No newline at end of file +} \ No newline at end of file