diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/AiChatService.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/AiChatService.cs index 335697f5..06213cf2 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/AiChatService.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/AiChatService.cs @@ -16,6 +16,7 @@ using Yi.Framework.AiHub.Domain.Entities; using Yi.Framework.AiHub.Domain.Entities.Model; using Yi.Framework.AiHub.Domain.Extensions; using Yi.Framework.AiHub.Domain.Managers; +using Yi.Framework.AiHub.Domain.Shared.Consts; using Yi.Framework.AiHub.Domain.Shared.Dtos; using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi; using Yi.Framework.AiHub.Domain.Shared.Enums; @@ -35,17 +36,19 @@ public class AiChatService : ApplicationService private readonly AiBlacklistManager _aiBlacklistManager; private readonly ILogger _logger; private readonly AiGateWayManager _aiGateWayManager; + private readonly PremiumPackageManager _premiumPackageManager; public AiChatService(IHttpContextAccessor httpContextAccessor, AiBlacklistManager aiBlacklistManager, ISqlSugarRepository aiModelRepository, - ILogger logger, AiGateWayManager aiGateWayManager) + ILogger logger, AiGateWayManager aiGateWayManager, PremiumPackageManager premiumPackageManager) { _httpContextAccessor = httpContextAccessor; _aiBlacklistManager = aiBlacklistManager; _aiModelRepository = aiModelRepository; _logger = logger; _aiGateWayManager = aiGateWayManager; + _premiumPackageManager = premiumPackageManager; } @@ -118,6 +121,17 @@ public class AiChatService : ApplicationService } } + //如果是尊享包服务,需要校验是是否尊享包足够 + if (CurrentUser.IsAuthenticated && PremiumPackageConst.ModeIds.Contains(input.Model)) + { + // 检查尊享token包用量 + var availableTokens = await _premiumPackageManager.GetAvailableTokensAsync(CurrentUser.GetId()); + if (availableTokens <= 0) + { + throw new UserFriendlyException("尊享token包用量不足,请先购买尊享token包"); + } + } + //ai网关代理httpcontext await _aiGateWayManager.CompleteChatStreamForStatisticsAsync(_httpContextAccessor.HttpContext, input, CurrentUser.Id, sessionId, cancellationToken); @@ -154,7 +168,7 @@ public class AiChatService : ApplicationService { input.Model = "DeepSeek-R1-0528"; } - + //ai网关代理httpcontext await _aiGateWayManager.CompleteChatStreamForStatisticsAsync(_httpContextAccessor.HttpContext, input, CurrentUser.Id, null, cancellationToken); diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/OpenApiService.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/OpenApiService.cs index 8fba666b..f701bc26 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/OpenApiService.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/OpenApiService.cs @@ -57,6 +57,18 @@ public class OpenApiService : ApplicationService var httpContext = this._httpContextAccessor.HttpContext; var userId = await _tokenManager.GetUserIdAsync(GetTokenByHttpContext(httpContext)); await _aiBlacklistManager.VerifiyAiBlacklist(userId); + + //如果是尊享包服务,需要校验是是否尊享包足够 + if (PremiumPackageConst.ModeIds.Contains(input.Model)) + { + // 检查尊享token包用量 + var availableTokens = await _premiumPackageManager.GetAvailableTokensAsync(userId); + if (availableTokens <= 0) + { + throw new UserFriendlyException("尊享token包用量不足,请先购买尊享token包"); + } + } + //ai网关代理httpcontext if (input.Stream == true) { diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Consts/PremiumPackageConst.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Consts/PremiumPackageConst.cs new file mode 100644 index 00000000..b60039da --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Consts/PremiumPackageConst.cs @@ -0,0 +1,8 @@ +using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata; + +namespace Yi.Framework.AiHub.Domain.Shared.Consts; + +public class PremiumPackageConst +{ + public static List ModeIds = ["claude-sonnet-4-5-20250929"]; +} \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Enums/ModelApiTypeEnum.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Enums/ModelApiTypeEnum.cs new file mode 100644 index 00000000..aa342dce --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Enums/ModelApiTypeEnum.cs @@ -0,0 +1,7 @@ +namespace Yi.Framework.AiHub.Domain.Shared.Enums; + +public enum ModelApiTypeEnum +{ + OpenAi, + Claude +} \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiGateWay/Impl/ThorClaude/Chats/ClaudiaChatCompletionsService.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiGateWay/Impl/ThorClaude/Chats/ClaudiaChatCompletionsService.cs new file mode 100644 index 00000000..99ed2da0 --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiGateWay/Impl/ThorClaude/Chats/ClaudiaChatCompletionsService.cs @@ -0,0 +1,861 @@ +using System.Diagnostics; +using System.Net; +using System.Net.Http.Json; +using System.Text.Json; +using Microsoft.Extensions.Logging; +using Yi.Framework.AiHub.Domain.Shared.Dtos; +using Yi.Framework.AiHub.Domain.Shared.Dtos.Anthropic; +using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi; + +namespace Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorClaude.Chats; + +public sealed class ClaudiaChatCompletionsService( + IHttpClientFactory httpClientFactory, + ILogger logger) + : IChatCompletionService +{ + public List CreateResponse(AnthropicChatCompletionDto completionDto) + { + var response = new ThorChatChoiceResponse(); + var chatMessage = new ThorChatMessage(); + if (completionDto == null) + { + return new List(); + } + + if (completionDto.content.Any(x => x.type.Equals("thinking", StringComparison.OrdinalIgnoreCase))) + { + // 将推理字段合并到返回对象去 + chatMessage.ReasoningContent = completionDto.content + .First(x => x.type.Equals("thinking", StringComparison.OrdinalIgnoreCase)).Thinking; + + chatMessage.Role = completionDto.role; + chatMessage.Content = completionDto.content + .First(x => x.type.Equals("text", StringComparison.OrdinalIgnoreCase)).text; + } + else + { + chatMessage.Role = completionDto.role; + chatMessage.Content = completionDto.content + .FirstOrDefault()?.text; + } + + response.Delta = chatMessage; + response.Message = chatMessage; + + if (completionDto.content.Any(x => x.type.Equals("tool_use", StringComparison.OrdinalIgnoreCase))) + { + var toolUse = completionDto.content + .First(x => x.type.Equals("tool_use", StringComparison.OrdinalIgnoreCase)); + + chatMessage.ToolCalls = + [ + new() + { + Id = toolUse.id, + Function = new ThorChatMessageFunction() + { + Name = toolUse.name, + Arguments = JsonSerializer.Serialize(toolUse.input, + ThorJsonSerializer.DefaultOptions), + }, + Index = 0, + } + ]; + + return + [ + response + ]; + } + + return new List { response }; + } + + private object CreateMessage(List messages, AiModelDescribe options) + { + var list = new List(); + + foreach (var message in messages) + { + // 如果是图片 + if (message.ContentCalculated is IList contentCalculated) + { + list.Add(new + { + role = message.Role, + content = (List)contentCalculated.Select(x => + { + if (x.Type == "text") + { + if ("true".Equals(options.ModelExtraInfo, StringComparison.OrdinalIgnoreCase)) + { + return new + { + type = "text", + text = x.Text, + cache_control = new + { + type = "ephemeral" + } + }; + } + + return new + { + type = "text", + text = x.Text + }; + } + + var isBase64 = x.ImageUrl?.Url.StartsWith("http") == true; + + if ("true".Equals(options.ModelExtraInfo, StringComparison.OrdinalIgnoreCase)) + { + return new + { + type = "image", + source = new + { + type = isBase64 ? "base64" : "url", + media_type = "image/png", + data = x.ImageUrl?.Url, + }, + cache_control = new + { + type = "ephemeral" + } + }; + } + + return new + { + type = "image", + source = new + { + type = isBase64 ? "base64" : "url", + media_type = "image/png", + data = x.ImageUrl?.Url, + } + }; + }) + }); + } + else + { + if ("true".Equals(options.ModelExtraInfo, StringComparison.OrdinalIgnoreCase)) + { + if (message.Role == "system") + { + list.Add(new + { + type = "text", + text = message.Content, + cache_control = new + { + type = "ephemeral" + } + }); + } + else + { + list.Add(new + { + role = message.Role, + content = message.Content + }); + } + } + else + { + if (message.Role == "system") + { + list.Add(new + { + type = "text", + text = message.Content + }); + } + else if (message.Role == "tool") + { + list.Add(new + { + role = "user", + content = new List + { + new + { + type = "tool_result", + tool_use_id = message.ToolCallId, + content = message.Content + } + } + }); + } + else if (message.Role == "assistant") + { + // { + // "role": "assistant", + // "content": [ + // { + // "type": "text", + // "text": "I need to use get_weather, and the user wants SF, which is likely San Francisco, CA." + // }, + // { + // "type": "tool_use", + // "id": "toolu_01A09q90qw90lq917835lq9", + // "name": "get_weather", + // "input": { + // "location": "San Francisco, CA", + // "unit": "celsius" + // } + // } + // ] + // }, + if (message.ToolCalls?.Count > 0) + { + var content = new List(); + if (!string.IsNullOrEmpty(message.Content)) + { + content.Add(new + { + type = "text", + text = message.Content + }); + } + + foreach (var toolCall in message.ToolCalls) + { + content.Add(new + { + type = "tool_use", + id = toolCall.Id, + name = toolCall.Function?.Name, + input = JsonSerializer.Deserialize>( + toolCall.Function?.Arguments, ThorJsonSerializer.DefaultOptions) + }); + } + + list.Add(new + { + role = "assistant", + content + }); + } + else + { + list.Add(new + { + role = "assistant", + content = new List + { + new + { + type = "text", + text = message.Content + } + } + }); + } + } + else + { + list.Add(new + { + role = message.Role, + content = message.Content + }); + } + } + } + } + + return list; + } + + + public async IAsyncEnumerable CompleteChatStreamAsync(AiModelDescribe options, + ThorChatCompletionsRequest input, + CancellationToken cancellationToken) + { + using var openai = + Activity.Current?.Source.StartActivity("Claudia 对话补全"); + + if (string.IsNullOrEmpty(options.Endpoint)) + { + options.Endpoint = "https://api.anthropic.com/"; + } + + var client = httpClientFactory.CreateClient(); + + var headers = new Dictionary + { + { "x-api-key", options.ApiKey }, + { "anthropic-version", "2023-06-01" } + }; + + var isThinking = input.Model.EndsWith("thinking"); + input.Model = input.Model.Replace("-thinking", string.Empty); + var budgetTokens = 1024; + + if (input.MaxTokens is < 2048) + { + input.MaxTokens = 2048; + } + + if (input.MaxTokens != null && input.MaxTokens / 2 < 1024) + { + budgetTokens = input.MaxTokens.Value / (4 * 3); + } + + // budgetTokens最大4096 + budgetTokens = Math.Min(budgetTokens, 4096); + + object tool_choice; + if (input.ToolChoice is not null && input.ToolChoice.Type == "auto") + { + tool_choice = new + { + type = "auto", + disable_parallel_tool_use = false, + }; + } + else if (input.ToolChoice is not null && input.ToolChoice.Type == "any") + { + tool_choice = new + { + type = "any", + disable_parallel_tool_use = false, + }; + } + else if (input.ToolChoice is not null && input.ToolChoice.Type == "tool") + { + tool_choice = new + { + type = "tool", + name = input.ToolChoice.Function?.Name, + disable_parallel_tool_use = false, + }; + } + else + { + tool_choice = null; + } + + var response = await client.HttpRequestRaw(options.Endpoint.TrimEnd('/') + "/v1/messages", new + { + model = input.Model, + max_tokens = input.MaxTokens ?? 2048, + stream = true, + tool_choice, + system = CreateMessage(input.Messages.Where(x => x.Role == "system").ToList(), options), + messages = CreateMessage(input.Messages.Where(x => x.Role != "system").ToList(), options), + top_p = isThinking ? null : input.TopP, + thinking = isThinking + ? new + { + type = "enabled", + budget_tokens = budgetTokens, + } + : null, + tools = input.Tools?.Select(x => new + { + name = x.Function?.Name, + description = x.Function?.Description, + input_schema = new + { + type = x.Function?.Parameters?.Type, + required = x.Function?.Parameters?.Required, + properties = x.Function?.Parameters?.Properties?.ToDictionary(y => y.Key, y => new + { + description = y.Value.Description, + type = y.Value.Type, + @enum = y.Value.Enum + }) + } + }).ToArray(), + temperature = isThinking ? null : input.Temperature + }, string.Empty, headers); + + openai?.SetTag("Model", input.Model); + openai?.SetTag("Response", response.StatusCode.ToString()); + + // 大于等于400的状态码都认为是异常 + if (response.StatusCode >= HttpStatusCode.BadRequest) + { + var error = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); + logger.LogError("OpenAI对话异常 请求地址:{Address}, StatusCode: {StatusCode} Response: {Response}", + options.Endpoint, + response.StatusCode, error); + + throw new Exception("OpenAI对话异常" + response.StatusCode); + } + + using var stream = new StreamReader(await response.Content.ReadAsStreamAsync(cancellationToken)); + + using StreamReader reader = new(await response.Content.ReadAsStreamAsync(cancellationToken)); + string? line = string.Empty; + var first = true; + var isThink = false; + + string? toolId = null; + string? toolName = null; + while ((line = await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false)) != null) + { + line += Environment.NewLine; + + if (line.StartsWith('{')) + { + logger.LogInformation("OpenAI对话异常 , StatusCode: {StatusCode} Response: {Response}", response.StatusCode, + line); + + throw new Exception("OpenAI对话异常" + line); + } + + if (line.StartsWith(OpenAIConstant.Data)) + line = line[OpenAIConstant.Data.Length..]; + + line = line.Trim(); + + if (string.IsNullOrWhiteSpace(line)) continue; + + if (line == OpenAIConstant.Done) + { + break; + } + + if (line.StartsWith(':')) + { + continue; + } + + if (line.StartsWith("event: ", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + var result = JsonSerializer.Deserialize(line, + ThorJsonSerializer.DefaultOptions); + + if (result?.Type == "content_block_delta") + { + if (result.Delta.Type is "text" or "text_delta") + { + yield return new ThorChatCompletionsResponse() + { + Choices = + [ + new() + { + Message = new ThorChatMessage() + { + Content = result.Delta.Text, + Role = "assistant", + } + } + ], + Model = input.Model, + Id = result?.Message?.id, + Usage = new ThorUsageResponse() + { + CompletionTokens = result?.Message?.Usage?.OutputTokens, + PromptTokens = result?.Message?.Usage?.InputTokens, + } + }; + } + else if (result.Delta.Type == "input_json_delta") + { + yield return new ThorChatCompletionsResponse() + { + Choices = + [ + new ThorChatChoiceResponse() + { + Message = new ThorChatMessage() + { + ToolCalls = + [ + new ThorToolCall() + { + Id = toolId, + Function = new ThorChatMessageFunction() + { + Name = toolName, + Arguments = result.Delta.PartialJson + } + } + ], + Role = "tool", + } + } + ], + Model = input.Model, + Usage = new ThorUsageResponse() + { + PromptTokens = result?.Message?.Usage?.InputTokens, + } + }; + } + else + { + yield return new ThorChatCompletionsResponse() + { + Choices = new List() + { + new() + { + Message = new ThorChatMessage() + { + ReasoningContent = result.Delta.Thinking, + Role = "assistant", + } + } + }, + Model = input.Model, + Id = result?.Message?.id, + Usage = new ThorUsageResponse() + { + CompletionTokens = result?.Message?.Usage?.OutputTokens, + PromptTokens = result?.Message?.Usage?.InputTokens + } + }; + } + + continue; + } + + if (result?.Type == "content_block_start") + { + if (result?.ContentBlock?.Id is not null) + { + toolId = result.ContentBlock.Id; + } + + if (result?.ContentBlock?.Name is not null) + { + toolName = result.ContentBlock.Name; + } + + yield return new ThorChatCompletionsResponse() + { + Choices = + [ + new ThorChatChoiceResponse() + { + Message = new ThorChatMessage() + { + ToolCalls = + [ + new ThorToolCall() + { + Id = toolId, + Function = new ThorChatMessageFunction() + { + Name = toolName + } + } + ], + Role = "tool", + } + } + ], + Model = input.Model, + Usage = new ThorUsageResponse() + { + PromptTokens = result?.Message?.Usage?.InputTokens, + } + }; + } + + if (result.Type == "content_block_delta") + { + yield return new ThorChatCompletionsResponse() + { + Choices = + [ + new ThorChatChoiceResponse() + { + Message = new ThorChatMessage() + { + ToolCallId = result?.ContentBlock?.Id, + FunctionCall = new ThorChatMessageFunction() + { + Name = result?.ContentBlock?.Name, + Arguments = result?.Delta?.PartialJson + }, + Role = "tool", + } + } + ], + Model = input.Model, + Usage = new ThorUsageResponse() + { + PromptTokens = result?.Message?.Usage?.InputTokens, + } + }; + continue; + } + + if (result.Type == "message_start") + { + yield return new ThorChatCompletionsResponse() + { + Choices = + [ + new ThorChatChoiceResponse() + { + Message = new ThorChatMessage() + { + Content = result?.Delta?.Text, + Role = "assistant", + } + } + ], + Model = input.Model, + Usage = new ThorUsageResponse() + { + PromptTokens = result?.Message?.Usage?.InputTokens, + } + }; + + continue; + } + + if (result.Type == "message_delta") + { + yield return new ThorChatCompletionsResponse() + { + Choices = + [ + new ThorChatChoiceResponse() + { + Message = new ThorChatMessage() + { + Content = result.Delta?.Text, + Role = "assistant", + } + } + ], + Model = input.Model, + Usage = new ThorUsageResponse() + { + CompletionTokens = result.Usage?.OutputTokens, + InputTokens = result.Usage?.InputTokens + } + }; + + continue; + } + + if (result.Message == null) + { + continue; + } + + var chat = CreateResponse(result.Message); + + var content = chat?.FirstOrDefault()?.Delta; + + if (first && string.IsNullOrWhiteSpace(content?.Content) && string.IsNullOrEmpty(content?.ReasoningContent)) + { + continue; + } + + if (first && content.Content == OpenAIConstant.ThinkStart) + { + isThink = true; + continue; + // 需要将content的内容转换到其他字段 + } + + if (isThink && content.Content.Contains(OpenAIConstant.ThinkEnd)) + { + isThink = false; + // 需要将content的内容转换到其他字段 + continue; + } + + if (isThink) + { + // 需要将content的内容转换到其他字段 + foreach (var choice in chat) + { + choice.Delta.ReasoningContent = choice.Delta.Content; + choice.Delta.Content = string.Empty; + } + } + + first = false; + + yield return new ThorChatCompletionsResponse() + { + Choices = chat, + Model = input.Model, + Id = result.Message.id, + Usage = new ThorUsageResponse() + { + CompletionTokens = result.Message.Usage?.OutputTokens, + PromptTokens = result.Message.Usage?.InputTokens, + InputTokens = result.Message.Usage?.InputTokens, + OutputTokens = result.Message.Usage?.OutputTokens, + TotalTokens = result.Message.Usage?.InputTokens + result.Message.Usage?.OutputTokens + } + }; + } + } + + public async Task CompleteChatAsync(AiModelDescribe options, + ThorChatCompletionsRequest input, + CancellationToken cancellationToken) + { + using var openai = + Activity.Current?.Source.StartActivity("Claudia 对话补全"); + + if (string.IsNullOrEmpty(options.Endpoint)) + { + options.Endpoint = "https://api.anthropic.com/"; + } + + var client = httpClientFactory.CreateClient(); + + var headers = new Dictionary + { + { "x-api-key", options.ApiKey }, + { "anthropic-version", "2023-06-01" } + }; + + bool isThink = input.Model.EndsWith("-thinking"); + input.Model = input.Model.Replace("-thinking", string.Empty); + + var budgetTokens = 1024; + if (input.MaxTokens is < 2048) + { + input.MaxTokens = 2048; + } + + if (input.MaxTokens != null && input.MaxTokens / 2 < 1024) + { + budgetTokens = input.MaxTokens.Value / (4 * 3); + } + + object tool_choice; + if (input.ToolChoice is not null && input.ToolChoice.Type == "auto") + { + tool_choice = new + { + type = "auto", + disable_parallel_tool_use = false, + }; + } + else if (input.ToolChoice is not null && input.ToolChoice.Type == "any") + { + tool_choice = new + { + type = "any", + disable_parallel_tool_use = false, + }; + } + else if (input.ToolChoice is not null && input.ToolChoice.Type == "tool") + { + tool_choice = new + { + type = "tool", + name = input.ToolChoice.Function?.Name, + disable_parallel_tool_use = false, + }; + } + else + { + tool_choice = null; + } + + // budgetTokens最大4096 + budgetTokens = Math.Min(budgetTokens, 4096); + + var response = await client.PostJsonAsync(options.Endpoint.TrimEnd('/') + "/v1/messages", new + { + model = input.Model, + max_tokens = input.MaxTokens ?? 2000, + system = CreateMessage(input.Messages.Where(x => x.Role == "system").ToList(), options), + messages = CreateMessage(input.Messages.Where(x => x.Role != "system").ToList(), options), + top_p = isThink ? null : input.TopP, + tool_choice, + thinking = isThink + ? new + { + type = "enabled", + budget_tokens = budgetTokens, + } + : null, + tools = input.Tools?.Select(x => new + { + name = x.Function?.Name, + description = x.Function?.Description, + input_schema = new + { + type = x.Function?.Parameters?.Type, + required = x.Function?.Parameters?.Required, + properties = x.Function?.Parameters?.Properties?.ToDictionary(y => y.Key, y => new + { + description = y.Value.Description, + type = y.Value.Type, + @enum = y.Value.Enum + }) + } + }).ToArray(), + temperature = isThink ? null : input.Temperature + }, string.Empty, headers); + + openai?.SetTag("Model", input.Model); + openai?.SetTag("Response", response.StatusCode.ToString()); + + // 大于等于400的状态码都认为是异常 + if (response.StatusCode >= HttpStatusCode.BadRequest) + { + var error = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); + logger.LogError("OpenAI对话异常 请求地址:{Address}, StatusCode: {StatusCode} Response: {Response}", + options.Endpoint, + response.StatusCode, error); + + throw new Exception("OpenAI对话异常" + response.StatusCode.ToString()); + } + + var value = + await response.Content.ReadFromJsonAsync(ThorJsonSerializer.DefaultOptions, + cancellationToken: cancellationToken); + + var thor = new ThorChatCompletionsResponse() + { + Choices = CreateResponse(value), + Model = input.Model, + Id = value.id, + Usage = new ThorUsageResponse() + { + CompletionTokens = value.Usage.OutputTokens, + PromptTokens = value.Usage.InputTokens + } + }; + + if (value.Usage.CacheReadInputTokens != null) + { + thor.Usage.PromptTokensDetails ??= new ThorUsageResponsePromptTokensDetails() + { + CachedTokens = value.Usage.CacheReadInputTokens.Value, + }; + + if (value.Usage.InputTokens > 0) + { + thor.Usage.InputTokens = value.Usage.InputTokens; + } + + if (value.Usage.OutputTokens > 0) + { + thor.Usage.CompletionTokens = value.Usage.OutputTokens; + thor.Usage.OutputTokens = value.Usage.OutputTokens; + } + } + + thor.Usage.TotalTokens = thor.Usage.InputTokens + thor.Usage.OutputTokens; + return thor; + } +} \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/Model/AiModelEntity.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/Model/AiModelEntity.cs index b1149eb8..3abd9b70 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/Model/AiModelEntity.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/Model/AiModelEntity.cs @@ -52,7 +52,12 @@ public class AiModelEntity : Entity, IOrderNum, ISoftDelete public string? ExtraInfo { get; set; } /// - /// 模型类型 + /// 模型类型(聊天/图片等) /// public ModelTypeEnum ModelType { get; set; } + + /// + /// 模型Api类型,现支持同一个模型id,多种接口格式 + /// + public ModelApiTypeEnum ModelApiType { get; set; } } \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/AiGateWayManager.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/AiGateWayManager.cs index e81a21cd..5071ab4f 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/AiGateWayManager.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/AiGateWayManager.cs @@ -12,11 +12,13 @@ using Volo.Abp.Domain.Services; using Yi.Framework.AiHub.Domain.AiGateWay; using Yi.Framework.AiHub.Domain.AiGateWay.Exceptions; using Yi.Framework.AiHub.Domain.Entities.Model; +using Yi.Framework.AiHub.Domain.Shared.Consts; using Yi.Framework.AiHub.Domain.Shared.Dtos; using Yi.Framework.AiHub.Domain.Shared.Dtos.Anthropic; using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi; using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi.Embeddings; using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi.Images; +using Yi.Framework.AiHub.Domain.Shared.Enums; using Yi.Framework.Core.Extensions; using Yi.Framework.SqlSugarCore.Abstractions; using JsonSerializer = System.Text.Json.JsonSerializer; @@ -27,21 +29,24 @@ namespace Yi.Framework.AiHub.Domain.Managers; public class AiGateWayManager : DomainService { private readonly ISqlSugarRepository _aiAppRepository; + private readonly ISqlSugarRepository _aiModelRepository; private readonly ILogger _logger; private readonly AiMessageManager _aiMessageManager; private readonly UsageStatisticsManager _usageStatisticsManager; private readonly ISpecialCompatible _specialCompatible; private PremiumPackageManager? _premiumPackageManager; + public AiGateWayManager(ISqlSugarRepository aiAppRepository, ILogger logger, AiMessageManager aiMessageManager, UsageStatisticsManager usageStatisticsManager, - ISpecialCompatible specialCompatible) + ISpecialCompatible specialCompatible, ISqlSugarRepository aiModelRepository) { _aiAppRepository = aiAppRepository; _logger = logger; _aiMessageManager = aiMessageManager; _usageStatisticsManager = usageStatisticsManager; _specialCompatible = specialCompatible; + _aiModelRepository = aiModelRepository; } private PremiumPackageManager PremiumPackageManager => @@ -50,17 +55,17 @@ public class AiGateWayManager : DomainService /// /// 获取模型 /// + /// /// /// - private async Task GetModelAsync(string modelId) + private async Task GetModelAsync(ModelApiTypeEnum modelApiType, string modelId) { - var allApp = await _aiAppRepository._DbQueryable.Includes(x => x.AiModels).ToListAsync(); - foreach (var app in allApp) - { - var model = app.AiModels.FirstOrDefault(x => x.ModelId == modelId); - if (model is not null) - { - return new AiModelDescribe + var aiModelDescribe = await _aiModelRepository._DbQueryable + .LeftJoin((model, app) => model.AiAppId == app.Id) + .Where((model, app) => model.ModelId == modelId) + .Where((model, app) => model.ModelApiType == modelApiType) + .Select((model, app) => + new AiModelDescribe { AppId = app.Id, AppName = app.Name, @@ -73,11 +78,14 @@ public class AiGateWayManager : DomainService Description = model.Description, AppExtraUrl = app.ExtraUrl, ModelExtraInfo = model.ExtraInfo - }; - } + }) + .FirstAsync(); + if (aiModelDescribe is null) + { + throw new UserFriendlyException($"【{modelId}】模型当前版本【{modelApiType}】格式不支持"); } - throw new UserFriendlyException($"{modelId}模型当前版本不支持"); + return aiModelDescribe; } @@ -92,7 +100,7 @@ public class AiGateWayManager : DomainService [EnumeratorCancellation] CancellationToken cancellationToken) { _specialCompatible.Compatible(request); - var modelDescribe = await GetModelAsync(request.Model); + var modelDescribe = await GetModelAsync(ModelApiTypeEnum.OpenAi, request.Model); var chatService = LazyServiceProvider.GetRequiredKeyedService(modelDescribe.HandlerName); @@ -122,7 +130,7 @@ public class AiGateWayManager : DomainService var response = httpContext.Response; // 设置响应头,声明是 json //response.ContentType = "application/json; charset=UTF-8"; - var modelDescribe = await GetModelAsync(request.Model); + var modelDescribe = await GetModelAsync(ModelApiTypeEnum.OpenAi, request.Model); var chatService = LazyServiceProvider.GetRequiredKeyedService(modelDescribe.HandlerName); var data = await chatService.CompleteChatAsync(modelDescribe, request, cancellationToken); @@ -277,6 +285,16 @@ public class AiGateWayManager : DomainService }); await _usageStatisticsManager.SetUsageAsync(userId, request.Model, tokenUsage); + + // 扣减尊享token包用量 + if (userId is not null && PremiumPackageConst.ModeIds.Contains(request.Model)) + { + var totalTokens = tokenUsage.TotalTokens ?? 0; + if (totalTokens > 0) + { + await PremiumPackageManager.ConsumeTokensAsync(userId.Value, totalTokens); + } + } } @@ -297,7 +315,7 @@ public class AiGateWayManager : DomainService var model = request.Model; if (string.IsNullOrEmpty(model)) model = "dall-e-2"; - var modelDescribe = await GetModelAsync(model); + var modelDescribe = await GetModelAsync(ModelApiTypeEnum.OpenAi, model); // 获取渠道指定的实现类型的服务 var imageService = @@ -329,6 +347,16 @@ public class AiGateWayManager : DomainService }); await _usageStatisticsManager.SetUsageAsync(userId, model, response.Usage); + + // 扣减尊享token包用量 + if (userId is not null && PremiumPackageConst.ModeIds.Contains(request.Model)) + { + var totalTokens = response.Usage.TotalTokens ?? 0; + if (totalTokens > 0) + { + await PremiumPackageManager.ConsumeTokensAsync(userId.Value, totalTokens); + } + } } catch (Exception e) { @@ -357,7 +385,7 @@ public class AiGateWayManager : DomainService using var embedding = Activity.Current?.Source.StartActivity("向量模型调用"); - var modelDescribe = await GetModelAsync(input.Model); + var modelDescribe = await GetModelAsync(ModelApiTypeEnum.OpenAi, input.Model); // 获取渠道指定的实现类型的服务 var embeddingService = @@ -461,7 +489,7 @@ public class AiGateWayManager : DomainService [EnumeratorCancellation] CancellationToken cancellationToken) { _specialCompatible.AnthropicCompatible(request); - var modelDescribe = await GetModelAsync(request.Model); + var modelDescribe = await GetModelAsync(ModelApiTypeEnum.Claude, request.Model); var chatService = LazyServiceProvider.GetRequiredKeyedService(modelDescribe.HandlerName); @@ -491,7 +519,7 @@ public class AiGateWayManager : DomainService var response = httpContext.Response; // 设置响应头,声明是 json //response.ContentType = "application/json; charset=UTF-8"; - var modelDescribe = await GetModelAsync(request.Model); + var modelDescribe = await GetModelAsync(ModelApiTypeEnum.Claude, request.Model); var chatService = LazyServiceProvider.GetRequiredKeyedService(modelDescribe.HandlerName); var data = await chatService.ChatCompletionsAsync(modelDescribe, request, cancellationToken); @@ -516,14 +544,10 @@ public class AiGateWayManager : DomainService await _usageStatisticsManager.SetUsageAsync(userId.Value, request.Model, data.TokenUsage); // 扣减尊享token包用量 - var totalTokens = data.TokenUsage.TotalTokens??0; + var totalTokens = data.TokenUsage.TotalTokens ?? 0; if (totalTokens > 0) { - var consumeSuccess = await PremiumPackageManager.ConsumeTokensAsync(userId.Value, totalTokens); - if (!consumeSuccess) - { - _logger.LogWarning($"用户 {userId.Value} 尊享token包扣减失败,消耗token数: {totalTokens}"); - } + await PremiumPackageManager.ConsumeTokensAsync(userId.Value, totalTokens); } } @@ -562,10 +586,11 @@ public class AiGateWayManager : DomainService await foreach (var responseResult in completeChatResponse) { //message_start是为了保底机制 - if (responseResult.Item1.Contains("message_delta")||responseResult.Item1.Contains("message_start")) + if (responseResult.Item1.Contains("message_delta") || responseResult.Item1.Contains("message_start")) { tokenUsage = responseResult.Item2?.TokenUsage; } + backupSystemContent.Append(responseResult.Item2?.Delta?.Text); await WriteAsEventStreamDataAsync(httpContext, responseResult.Item1, responseResult.Item2, cancellationToken); @@ -622,7 +647,7 @@ public class AiGateWayManager : DomainService // 扣减尊享token包用量 if (userId.HasValue && tokenUsage is not null) { - var totalTokens = tokenUsage.TotalTokens??0; + var totalTokens = tokenUsage.TotalTokens ?? 0; if (totalTokens > 0) { var consumeSuccess = await PremiumPackageManager.ConsumeTokensAsync(userId.Value, totalTokens); diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/YiFrameworkAiHubDomainModule.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/YiFrameworkAiHubDomainModule.cs index 8f7273d4..a4d506cd 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/YiFrameworkAiHubDomainModule.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/YiFrameworkAiHubDomainModule.cs @@ -42,7 +42,8 @@ namespace Yi.Framework.AiHub.Domain nameof(DeepSeekChatCompletionsService)); services.AddKeyedTransient( nameof(OpenAiChatCompletionsService)); - + services.AddKeyedTransient( + nameof(ClaudiaChatCompletionsService)); #endregion #region Anthropic ChatCompletion