From f90105ebb411ede4180ad023d817b26662ec2697 Mon Sep 17 00:00:00 2001 From: chenchun Date: Thu, 11 Dec 2025 17:33:12 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=85=A8=E7=AB=99=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{ => Activities}/CardFlipService.cs | 0 .../{ => Activities}/DailyTaskService.cs | 0 .../Services/OpenApiService.cs | 2 + .../Dtos/Anthropic/AnthropicInput.cs | 2 +- .../Dtos/Anthropic/AnthropicToOpenAI.cs | 648 ------------------ ...omOpenAIAnthropicChatCompletionsService.cs | 289 -------- .../Managers/AiGateWayManager.cs | 8 +- .../YiFrameworkAiHubDomainModule.cs | 4 +- 8 files changed, 8 insertions(+), 945 deletions(-) rename Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/{ => Activities}/CardFlipService.cs (100%) rename Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/{ => Activities}/DailyTaskService.cs (100%) delete mode 100644 Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Dtos/Anthropic/AnthropicToOpenAI.cs delete mode 100644 Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiGateWay/Impl/ThorCustomOpenAI/Chats/CustomOpenAIAnthropicChatCompletionsService.cs diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/CardFlipService.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/Activities/CardFlipService.cs similarity index 100% rename from Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/CardFlipService.cs rename to Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/Activities/CardFlipService.cs diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/DailyTaskService.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/Activities/DailyTaskService.cs similarity index 100% rename from Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/DailyTaskService.cs rename to Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/Activities/DailyTaskService.cs 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 0ccf376a..3649e5ad 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 @@ -201,6 +201,7 @@ public class OpenApiService : ApplicationService } } + /// /// 响应-Openai新规范 (尊享服务专用) /// @@ -252,6 +253,7 @@ public class OpenApiService : ApplicationService } } + #region 私有 private string? GetTokenByHttpContext(HttpContext httpContext) diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Dtos/Anthropic/AnthropicInput.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Dtos/Anthropic/AnthropicInput.cs index 12474b20..1432a6c0 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Dtos/Anthropic/AnthropicInput.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Dtos/Anthropic/AnthropicInput.cs @@ -12,7 +12,7 @@ public sealed class AnthropicInput [JsonPropertyName("max_tokens")] public int? MaxTokens { get; set; } - [JsonPropertyName("messages")] public IList Messages { get; set; } + [JsonPropertyName("messages")] public JsonElement? Messages { get; set; } [JsonPropertyName("tools")] public IList? Tools { get; set; } diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Dtos/Anthropic/AnthropicToOpenAI.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Dtos/Anthropic/AnthropicToOpenAI.cs deleted file mode 100644 index 9f925f9e..00000000 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Dtos/Anthropic/AnthropicToOpenAI.cs +++ /dev/null @@ -1,648 +0,0 @@ -using System.Text.Json; -using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi; - -namespace Yi.Framework.AiHub.Domain.Shared.Dtos.Anthropic; - -public static class AnthropicToOpenAi -{ - /// - /// 将AnthropicInput转换为ThorChatCompletionsRequest - /// - public static ThorChatCompletionsRequest ConvertAnthropicToOpenAi(AnthropicInput anthropicInput) - { - var openAiRequest = new ThorChatCompletionsRequest - { - Model = anthropicInput.Model, - MaxTokens = anthropicInput.MaxTokens, - Stream = anthropicInput.Stream, - Messages = new List(anthropicInput.Messages.Count) - }; - - // high medium minimal low - if (openAiRequest.Model.EndsWith("-high") || - openAiRequest.Model.EndsWith("-medium") || - openAiRequest.Model.EndsWith("-minimal") || - openAiRequest.Model.EndsWith("-low")) - { - openAiRequest.ReasoningEffort = openAiRequest.Model switch - { - var model when model.EndsWith("-high") => "high", - var model when model.EndsWith("-medium") => "medium", - var model when model.EndsWith("-minimal") => "minimal", - var model when model.EndsWith("-low") => "low", - _ => "medium" - }; - - openAiRequest.Model = openAiRequest.Model.Replace("-high", "") - .Replace("-medium", "") - .Replace("-minimal", "") - .Replace("-low", ""); - } - - if (anthropicInput.Thinking != null && - anthropicInput.Thinking.Type.Equals("enabled", StringComparison.OrdinalIgnoreCase)) - { - openAiRequest.Thinking = new ThorChatClaudeThinking() - { - BudgetToken = anthropicInput.Thinking.BudgetTokens, - Type = "enabled", - }; - openAiRequest.EnableThinking = true; - } - - if (openAiRequest.Model.EndsWith("-thinking")) - { - openAiRequest.EnableThinking = true; - openAiRequest.Model = openAiRequest.Model.Replace("-thinking", ""); - } - - if (openAiRequest.Stream == true) - { - openAiRequest.StreamOptions = new ThorStreamOptions() - { - IncludeUsage = true, - }; - } - - if (!string.IsNullOrEmpty(anthropicInput.System)) - { - openAiRequest.Messages.Add(ThorChatMessage.CreateSystemMessage(anthropicInput.System)); - } - - if (anthropicInput.Systems?.Count > 0) - { - foreach (var systemContent in anthropicInput.Systems) - { - openAiRequest.Messages.Add(ThorChatMessage.CreateSystemMessage(systemContent.Text ?? string.Empty)); - } - } - - // 处理messages - if (anthropicInput.Messages != null) - { - foreach (var message in anthropicInput.Messages) - { - var thorMessages = ConvertAnthropicMessageToThor(message); - // 需要过滤 空消息 - if (thorMessages.Count == 0) - { - continue; - } - - openAiRequest.Messages.AddRange(thorMessages); - } - - openAiRequest.Messages = openAiRequest.Messages - .Where(m => !string.IsNullOrEmpty(m.Content) || m.Contents?.Count > 0 || m.ToolCalls?.Count > 0 || - !string.IsNullOrEmpty(m.ToolCallId)) - .ToList(); - } - - // 处理tools - if (anthropicInput.Tools is { Count: > 0 }) - { - openAiRequest.Tools = anthropicInput.Tools.Where(x => x.name != "web_search") - .Select(ConvertAnthropicToolToThor).ToList(); - } - - // 判断是否存在web_search - if (anthropicInput.Tools?.Any(x => x.name == "web_search") == true) - { - openAiRequest.WebSearchOptions = new ThorChatWebSearchOptions() - { - }; - } - - // 处理tool_choice - if (anthropicInput.ToolChoice != null) - { - openAiRequest.ToolChoice = ConvertAnthropicToolChoiceToThor(anthropicInput.ToolChoice); - } - - return openAiRequest; - } - - /// - /// 根据最后的内容块类型和OpenAI的完成原因确定Claude的停止原因 - /// - public static string GetStopReasonByLastContentType(string? openAiFinishReason, string lastContentBlockType) - { - // 如果最后一个内容块是工具调用,优先返回tool_use - if (lastContentBlockType == "tool_use") - { - return "tool_use"; - } - - // 否则使用标准的转换逻辑 - return GetClaudeStopReason(openAiFinishReason); - } - - /// - /// 创建message_start事件 - /// - public static AnthropicStreamDto CreateMessageStartEvent(string messageId, string model) - { - return new AnthropicStreamDto - { - Type = "message_start", - Message = new AnthropicChatCompletionDto - { - id = messageId, - type = "message", - role = "assistant", - model = model, - content = new AnthropicChatCompletionDtoContent[0], - Usage = new AnthropicCompletionDtoUsage - { - InputTokens = 0, - OutputTokens = 0, - CacheCreationInputTokens = 0, - CacheReadInputTokens = 0 - } - } - }; - } - - /// - /// 创建content_block_start事件 - /// - public static AnthropicStreamDto CreateContentBlockStartEvent() - { - return new AnthropicStreamDto - { - Type = "content_block_start", - Index = 0, - ContentBlock = new AnthropicChatCompletionDtoContentBlock - { - Type = "text", - Id = null, - Name = null - } - }; - } - - /// - /// 创建thinking block start事件 - /// - public static AnthropicStreamDto CreateThinkingBlockStartEvent() - { - return new AnthropicStreamDto - { - Type = "content_block_start", - Index = 0, - ContentBlock = new AnthropicChatCompletionDtoContentBlock - { - Type = "thinking", - Id = null, - Name = null - } - }; - } - - /// - /// 创建content_block_delta事件 - /// - public static AnthropicStreamDto CreateContentBlockDeltaEvent(string text) - { - return new AnthropicStreamDto - { - Type = "content_block_delta", - Index = 0, - Delta = new AnthropicChatCompletionDtoDelta - { - Type = "text_delta", - Text = text - } - }; - } - - /// - /// 创建thinking delta事件 - /// - public static AnthropicStreamDto CreateThinkingBlockDeltaEvent(string thinking) - { - return new AnthropicStreamDto - { - Type = "content_block_delta", - Index = 0, - Delta = new AnthropicChatCompletionDtoDelta - { - Type = "thinking", - Thinking = thinking - } - }; - } - - /// - /// 创建content_block_stop事件 - /// - public static AnthropicStreamDto CreateContentBlockStopEvent() - { - return new AnthropicStreamDto - { - Type = "content_block_stop", - Index = 0 - }; - } - - /// - /// 创建message_delta事件 - /// - public static AnthropicStreamDto CreateMessageDeltaEvent(string finishReason, AnthropicCompletionDtoUsage usage) - { - return new AnthropicStreamDto - { - Type = "message_delta", - Usage = usage, - Delta = new AnthropicChatCompletionDtoDelta - { - StopReason = finishReason - } - }; - } - - /// - /// 创建message_stop事件 - /// - public static AnthropicStreamDto CreateMessageStopEvent() - { - return new AnthropicStreamDto - { - Type = "message_stop" - }; - } - - /// - /// 创建tool block start事件 - /// - public static AnthropicStreamDto CreateToolBlockStartEvent(string? toolId, string? toolName) - { - return new AnthropicStreamDto - { - Type = "content_block_start", - Index = 0, - ContentBlock = new AnthropicChatCompletionDtoContentBlock - { - Type = "tool_use", - Id = toolId, - Name = toolName - } - }; - } - - /// - /// 创建tool delta事件 - /// - public static AnthropicStreamDto CreateToolBlockDeltaEvent(string partialJson) - { - return new AnthropicStreamDto - { - Type = "content_block_delta", - Index = 0, - Delta = new AnthropicChatCompletionDtoDelta - { - Type = "input_json_delta", - PartialJson = partialJson - } - }; - } - - /// - /// 转换Anthropic消息为Thor消息列表 - /// - public static List ConvertAnthropicMessageToThor(AnthropicMessageInput anthropicMessage) - { - var results = new List(); - - // 处理简单的字符串内容 - if (anthropicMessage.Content != null) - { - var thorMessage = new ThorChatMessage - { - Role = anthropicMessage.Role, - Content = anthropicMessage.Content - }; - results.Add(thorMessage); - return results; - } - - // 处理多模态内容 - if (anthropicMessage.Contents is { Count: > 0 }) - { - var currentContents = new List(); - var currentToolCalls = new List(); - - foreach (var content in anthropicMessage.Contents) - { - switch (content.Type) - { - case "text": - currentContents.Add(ThorChatMessageContent.CreateTextContent(content.Text ?? string.Empty)); - break; - case "thinking" when !string.IsNullOrEmpty(content.Thinking): - results.Add(new ThorChatMessage() - { - ReasoningContent = content.Thinking - }); - break; - case "image": - { - if (content.Source != null) - { - var imageUrl = content.Source.Type == "base64" - ? $"data:{content.Source.MediaType};base64,{content.Source.Data}" - : content.Source.Data; - currentContents.Add(ThorChatMessageContent.CreateImageUrlContent(imageUrl ?? string.Empty)); - } - - break; - } - case "tool_use": - { - // 如果有普通内容,先创建内容消息 - if (currentContents.Count > 0) - { - if (currentContents.Count == 1 && currentContents.Any(x => x.Type == "text")) - { - var contentMessage = new ThorChatMessage - { - Role = anthropicMessage.Role, - ContentCalculated = currentContents.FirstOrDefault()?.Text ?? string.Empty - }; - results.Add(contentMessage); - } - else - { - var contentMessage = new ThorChatMessage - { - Role = anthropicMessage.Role, - Contents = currentContents - }; - results.Add(contentMessage); - } - - currentContents = new List(); - } - - // 收集工具调用 - var toolCall = new ThorToolCall - { - Id = content.Id, - Type = "function", - Function = new ThorChatMessageFunction - { - Name = content.Name, - Arguments = JsonSerializer.Serialize(content.Input) - } - }; - currentToolCalls.Add(toolCall); - break; - } - case "tool_result": - { - // 如果有普通内容,先创建内容消息 - if (currentContents.Count > 0) - { - var contentMessage = new ThorChatMessage - { - Role = anthropicMessage.Role, - Contents = currentContents - }; - results.Add(contentMessage); - currentContents = []; - } - - // 如果有工具调用,先创建工具调用消息 - if (currentToolCalls.Count > 0) - { - var toolCallMessage = new ThorChatMessage - { - Role = anthropicMessage.Role, - ToolCalls = currentToolCalls - }; - results.Add(toolCallMessage); - currentToolCalls = new List(); - } - - // 创建工具结果消息 - var toolMessage = new ThorChatMessage - { - Role = "tool", - ToolCallId = content.ToolUseId, - Content = content.Content?.ToString() ?? string.Empty - }; - results.Add(toolMessage); - break; - } - } - } - - // 处理剩余的内容 - if (currentContents.Count > 0) - { - var contentMessage = new ThorChatMessage - { - Role = anthropicMessage.Role, - Contents = currentContents - }; - results.Add(contentMessage); - } - - // 处理剩余的工具调用 - if (currentToolCalls.Count > 0) - { - var toolCallMessage = new ThorChatMessage - { - Role = anthropicMessage.Role, - ToolCalls = currentToolCalls - }; - results.Add(toolCallMessage); - } - } - - // 如果没有任何内容,返回一个空的消息 - if (results.Count == 0) - { - results.Add(new ThorChatMessage - { - Role = anthropicMessage.Role, - Content = string.Empty - }); - } - - // 如果只有一个text则使用content字段 - if (results is [{ Contents.Count: 1 }] && - results.FirstOrDefault()?.Contents?.FirstOrDefault()?.Type == "text" && - !string.IsNullOrEmpty(results.FirstOrDefault()?.Contents?.FirstOrDefault()?.Text)) - { - return - [ - new ThorChatMessage - { - Role = results[0].Role, - Content = results.FirstOrDefault()?.Contents?.FirstOrDefault()?.Text ?? string.Empty - } - ]; - } - - return results; - } - - /// - /// 转换Anthropic工具为Thor工具 - /// - public static ThorToolDefinition ConvertAnthropicToolToThor(AnthropicMessageTool anthropicTool) - { - IDictionary values = - new Dictionary(); - - if (anthropicTool.InputSchema?.Properties != null) - { - foreach (var property in anthropicTool.InputSchema.Properties) - { - if (property.Value?.description != null) - { - var definitionType = new ThorToolFunctionPropertyDefinition() - { - Description = property.Value.description, - Type = property.Value.type - }; - if (property.Value?.items?.type != null) - { - definitionType.Items = new ThorToolFunctionPropertyDefinition() - { - Type = property.Value.items.type - }; - } - - values.Add(property.Key, definitionType); - } - } - } - - - return new ThorToolDefinition - { - Type = "function", - Function = new ThorToolFunctionDefinition - { - Name = anthropicTool.name, - Description = anthropicTool.Description, - Parameters = new ThorToolFunctionPropertyDefinition - { - Type = anthropicTool.InputSchema?.Type ?? "object", - Properties = values, - Required = anthropicTool.InputSchema?.Required - } - } - }; - } - - /// - /// 将OpenAI的完成原因转换为Claude的停止原因 - /// - public static string GetClaudeStopReason(string? openAIFinishReason) - { - return openAIFinishReason switch - { - "stop" => "end_turn", - "length" => "max_tokens", - "tool_calls" => "tool_use", - "content_filter" => "stop_sequence", - _ => "end_turn" - }; - } - - /// - /// 将OpenAI响应转换为Claude响应格式 - /// - public static AnthropicChatCompletionDto ConvertOpenAIToClaude(ThorChatCompletionsResponse openAIResponse, - AnthropicInput originalRequest) - { - var claudeResponse = new AnthropicChatCompletionDto - { - id = openAIResponse.Id, - type = "message", - role = "assistant", - model = openAIResponse.Model ?? originalRequest.Model, - stop_reason = GetClaudeStopReason(openAIResponse.Choices?.FirstOrDefault()?.FinishReason), - stop_sequence = "", - content = [] - }; - - if (openAIResponse.Choices is { Count: > 0 }) - { - var choice = openAIResponse.Choices.First(); - var contents = new List(); - - if (!string.IsNullOrEmpty(choice.Message.Content) && !string.IsNullOrEmpty(choice.Message.ReasoningContent)) - { - contents.Add(new AnthropicChatCompletionDtoContent - { - type = "thinking", - Thinking = choice.Message.ReasoningContent - }); - - contents.Add(new AnthropicChatCompletionDtoContent - { - type = "text", - text = choice.Message.Content - }); - } - else - { - // 处理思维内容 - if (!string.IsNullOrEmpty(choice.Message.ReasoningContent)) - contents.Add(new AnthropicChatCompletionDtoContent - { - type = "thinking", - Thinking = choice.Message.ReasoningContent - }); - - // 处理文本内容 - if (!string.IsNullOrEmpty(choice.Message.Content)) - contents.Add(new AnthropicChatCompletionDtoContent - { - type = "text", - text = choice.Message.Content - }); - } - - // 处理工具调用 - if (choice.Message.ToolCalls is { Count: > 0 }) - contents.AddRange(choice.Message.ToolCalls.Select(toolCall => new AnthropicChatCompletionDtoContent - { - type = "tool_use", id = toolCall.Id, name = toolCall.Function?.Name, - input = JsonSerializer.Deserialize(toolCall.Function?.Arguments ?? "{}") - })); - - claudeResponse.content = contents.ToArray(); - } - - // 处理使用情况统计 - 确保始终提供Usage信息 - claudeResponse.Usage = new AnthropicCompletionDtoUsage - { - InputTokens = openAIResponse.Usage?.PromptTokens ?? 0, - OutputTokens = (int?)(openAIResponse.Usage?.CompletionTokens ?? 0), - CacheCreationInputTokens = openAIResponse.Usage?.PromptTokensDetails?.CachedTokens ?? 0, - CacheReadInputTokens = openAIResponse.Usage?.PromptTokensDetails?.CachedTokens ?? 0 - }; - - return claudeResponse; - } - - - /// - /// 转换Anthropic工具选择为Thor工具选择 - /// - public static ThorToolChoice ConvertAnthropicToolChoiceToThor(AnthropicTooChoiceInput anthropicToolChoice) - { - return new ThorToolChoice - { - Type = anthropicToolChoice.Type ?? "auto", - Function = anthropicToolChoice.Name != null - ? new ThorToolChoiceFunctionTool { Name = anthropicToolChoice.Name } - : null - }; - } -} \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiGateWay/Impl/ThorCustomOpenAI/Chats/CustomOpenAIAnthropicChatCompletionsService.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiGateWay/Impl/ThorCustomOpenAI/Chats/CustomOpenAIAnthropicChatCompletionsService.cs deleted file mode 100644 index 7240e37e..00000000 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiGateWay/Impl/ThorCustomOpenAI/Chats/CustomOpenAIAnthropicChatCompletionsService.cs +++ /dev/null @@ -1,289 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Volo.Abp.DependencyInjection; -using Yi.Framework.AiHub.Domain.Shared.Dtos; -using Yi.Framework.AiHub.Domain.Shared.Dtos.Anthropic; - -namespace Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorCustomOpenAI.Chats; - -/// -/// OpenAI到Claude适配器服务 -/// 将Claude格式的请求转换为OpenAI格式,然后将OpenAI的响应转换为Claude格式 -/// -public class CustomOpenAIAnthropicChatCompletionsService( - IAbpLazyServiceProvider serviceProvider, - ILogger logger) - : IAnthropicChatCompletionService -{ - private IChatCompletionService GetChatCompletionService() - { - return serviceProvider.GetRequiredKeyedService(nameof(OpenAiChatCompletionsService)); - } - - public async Task ChatCompletionsAsync(AiModelDescribe aiModelDescribe, - AnthropicInput request, - CancellationToken cancellationToken = default) - { - // 转换请求格式:Claude -> OpenAI - var openAIRequest = AnthropicToOpenAi.ConvertAnthropicToOpenAi(request); - - // 调用OpenAI服务 - var openAIResponse = - await GetChatCompletionService().CompleteChatAsync(aiModelDescribe,openAIRequest, cancellationToken); - - // 转换响应格式:OpenAI -> Claude - var claudeResponse = AnthropicToOpenAi.ConvertOpenAIToClaude(openAIResponse, request); - - return claudeResponse; - } - - public async IAsyncEnumerable<(string, AnthropicStreamDto?)> StreamChatCompletionsAsync(AiModelDescribe aiModelDescribe, - AnthropicInput request, - CancellationToken cancellationToken = default) - { - var openAIRequest = AnthropicToOpenAi.ConvertAnthropicToOpenAi(request); - openAIRequest.Stream = true; - - var messageId = Guid.NewGuid().ToString(); - var hasStarted = false; - var hasTextContentBlockStarted = false; - var hasThinkingContentBlockStarted = false; - var toolBlocksStarted = new Dictionary(); // 使用索引而不是ID - var toolCallIds = new Dictionary(); // 存储每个索引对应的ID - var toolCallIndexToBlockIndex = new Dictionary(); // 工具调用索引到块索引的映射 - var accumulatedUsage = new AnthropicCompletionDtoUsage(); - var isFinished = false; - var currentContentBlockType = ""; // 跟踪当前内容块类型 - var currentBlockIndex = 0; // 跟踪当前块索引 - var lastContentBlockType = ""; // 跟踪最后一个内容块类型,用于确定停止原因 - - await foreach (var openAIResponse in GetChatCompletionService().CompleteChatStreamAsync(aiModelDescribe,openAIRequest, - cancellationToken)) - { - // 发送message_start事件 - if (!hasStarted && openAIResponse.Choices?.Count > 0 && - openAIResponse.Choices.Any(x => x.Delta.ToolCalls?.Count > 0) == false) - { - hasStarted = true; - var messageStartEvent = AnthropicToOpenAi.CreateMessageStartEvent(messageId, request.Model); - yield return ("message_start", messageStartEvent); - } - - // 更新使用情况统计 - if (openAIResponse.Usage != null) - { - // 使用最新的token计数(OpenAI通常在最后的响应中提供完整的统计) - if (openAIResponse.Usage.PromptTokens.HasValue) - { - accumulatedUsage.InputTokens = openAIResponse.Usage.PromptTokens.Value; - } - - if (openAIResponse.Usage.CompletionTokens.HasValue) - { - accumulatedUsage.OutputTokens = (int)openAIResponse.Usage.CompletionTokens.Value; - } - - if (openAIResponse.Usage.PromptTokensDetails?.CachedTokens.HasValue == true) - { - accumulatedUsage.CacheReadInputTokens = - openAIResponse.Usage.PromptTokensDetails.CachedTokens.Value; - } - - // 记录调试信息 - logger.LogDebug("OpenAI Usage更新: Input={InputTokens}, Output={OutputTokens}, CacheRead={CacheRead}", - accumulatedUsage.InputTokens, accumulatedUsage.OutputTokens, - accumulatedUsage.CacheReadInputTokens); - } - - if (openAIResponse.Choices is { Count: > 0 }) - { - var choice = openAIResponse.Choices.First(); - - // 处理内容 - if (!string.IsNullOrEmpty(choice.Delta?.Content)) - { - // 如果当前有其他类型的内容块在运行,先结束它们 - if (currentContentBlockType != "text" && !string.IsNullOrEmpty(currentContentBlockType)) - { - var stopEvent = AnthropicToOpenAi.CreateContentBlockStopEvent(); - stopEvent.Index = currentBlockIndex; - yield return ("content_block_stop", stopEvent); - currentBlockIndex++; // 切换内容块时增加索引 - currentContentBlockType = ""; - } - - // 发送content_block_start事件(仅第一次) - if (!hasTextContentBlockStarted || currentContentBlockType != "text") - { - hasTextContentBlockStarted = true; - currentContentBlockType = "text"; - lastContentBlockType = "text"; - var contentBlockStartEvent = AnthropicToOpenAi.CreateContentBlockStartEvent(); - contentBlockStartEvent.Index = currentBlockIndex; - yield return ("content_block_start", - contentBlockStartEvent); - } - - // 发送content_block_delta事件 - var contentDeltaEvent = AnthropicToOpenAi.CreateContentBlockDeltaEvent(choice.Delta.Content); - contentDeltaEvent.Index = currentBlockIndex; - yield return ("content_block_delta", - contentDeltaEvent); - } - - // 处理工具调用 - if (choice.Delta?.ToolCalls is { Count: > 0 }) - { - foreach (var toolCall in choice.Delta.ToolCalls) - { - var toolCallIndex = toolCall.Index; // 使用索引来标识工具调用 - - // 发送tool_use content_block_start事件 - if (toolBlocksStarted.TryAdd(toolCallIndex, true)) - { - // 如果当前有文本或thinking内容块在运行,先结束它们 - if (currentContentBlockType == "text" || currentContentBlockType == "thinking") - { - var stopEvent = AnthropicToOpenAi.CreateContentBlockStopEvent(); - stopEvent.Index = currentBlockIndex; - yield return ("content_block_stop", stopEvent); - currentBlockIndex++; // 增加块索引 - } - // 如果当前有其他工具调用在运行,也需要结束它们 - else if (currentContentBlockType == "tool_use") - { - var stopEvent = AnthropicToOpenAi.CreateContentBlockStopEvent(); - stopEvent.Index = currentBlockIndex; - yield return ("content_block_stop", stopEvent); - currentBlockIndex++; // 增加块索引 - } - - currentContentBlockType = "tool_use"; - lastContentBlockType = "tool_use"; - - // 为此工具调用分配一个新的块索引 - toolCallIndexToBlockIndex[toolCallIndex] = currentBlockIndex; - - // 保存工具调用的ID(如果有的话) - if (!string.IsNullOrEmpty(toolCall.Id)) - { - toolCallIds[toolCallIndex] = toolCall.Id; - } - else if (!toolCallIds.ContainsKey(toolCallIndex)) - { - // 如果没有ID且之前也没有保存过,生成一个新的ID - toolCallIds[toolCallIndex] = Guid.NewGuid().ToString(); - } - - var toolBlockStartEvent = AnthropicToOpenAi.CreateToolBlockStartEvent( - toolCallIds[toolCallIndex], - toolCall.Function?.Name); - toolBlockStartEvent.Index = currentBlockIndex; - yield return ("content_block_start", - toolBlockStartEvent); - } - - // 如果有增量的参数,发送content_block_delta事件 - if (!string.IsNullOrEmpty(toolCall.Function?.Arguments)) - { - var toolDeltaEvent = - AnthropicToOpenAi.CreateToolBlockDeltaEvent(toolCall.Function.Arguments); - // 使用该工具调用对应的块索引 - toolDeltaEvent.Index = toolCallIndexToBlockIndex[toolCallIndex]; - yield return ("content_block_delta", - toolDeltaEvent); - } - } - } - - // 处理推理内容 - if (!string.IsNullOrEmpty(choice.Delta?.ReasoningContent)) - { - // 如果当前有其他类型的内容块在运行,先结束它们 - if (currentContentBlockType != "thinking" && !string.IsNullOrEmpty(currentContentBlockType)) - { - var stopEvent = AnthropicToOpenAi.CreateContentBlockStopEvent(); - stopEvent.Index = currentBlockIndex; - yield return ("content_block_stop", stopEvent); - currentBlockIndex++; // 增加块索引 - currentContentBlockType = ""; - } - - // 对于推理内容,也需要发送对应的事件 - if (!hasThinkingContentBlockStarted || currentContentBlockType != "thinking") - { - hasThinkingContentBlockStarted = true; - currentContentBlockType = "thinking"; - lastContentBlockType = "thinking"; - var thinkingBlockStartEvent = AnthropicToOpenAi.CreateThinkingBlockStartEvent(); - thinkingBlockStartEvent.Index = currentBlockIndex; - yield return ("content_block_start", - thinkingBlockStartEvent); - } - - var thinkingDeltaEvent = - AnthropicToOpenAi.CreateThinkingBlockDeltaEvent(choice.Delta.ReasoningContent); - thinkingDeltaEvent.Index = currentBlockIndex; - yield return ("content_block_delta", - thinkingDeltaEvent); - } - - // 处理结束 - if (!string.IsNullOrEmpty(choice.FinishReason) && !isFinished) - { - isFinished = true; - - // 发送content_block_stop事件(如果有活跃的内容块) - if (!string.IsNullOrEmpty(currentContentBlockType)) - { - var contentBlockStopEvent = AnthropicToOpenAi.CreateContentBlockStopEvent(); - contentBlockStopEvent.Index = currentBlockIndex; - yield return ("content_block_stop", - contentBlockStopEvent); - } - - // 发送message_delta事件 - var messageDeltaEvent = AnthropicToOpenAi.CreateMessageDeltaEvent( - AnthropicToOpenAi.GetStopReasonByLastContentType(choice.FinishReason, lastContentBlockType), - accumulatedUsage); - - // 记录最终Usage统计 - logger.LogDebug( - "流式响应结束,最终Usage: Input={InputTokens}, Output={OutputTokens}, CacheRead={CacheRead}", - accumulatedUsage.InputTokens, accumulatedUsage.OutputTokens, - accumulatedUsage.CacheReadInputTokens); - - yield return ("message_delta", - messageDeltaEvent); - - // 发送message_stop事件 - var messageStopEvent = AnthropicToOpenAi.CreateMessageStopEvent(); - yield return ("message_stop", - messageStopEvent); - } - } - } - - // 确保流正确结束 - if (!isFinished) - { - if (!string.IsNullOrEmpty(currentContentBlockType)) - { - var contentBlockStopEvent = AnthropicToOpenAi.CreateContentBlockStopEvent(); - contentBlockStopEvent.Index = currentBlockIndex; - yield return ("content_block_stop", - contentBlockStopEvent); - } - - var messageDeltaEvent = - AnthropicToOpenAi.CreateMessageDeltaEvent( - AnthropicToOpenAi.GetStopReasonByLastContentType("end_turn", lastContentBlockType), - accumulatedUsage); - yield return ("message_delta", messageDeltaEvent); - - var messageStopEvent = AnthropicToOpenAi.CreateMessageStopEvent(); - yield return ("message_stop", - messageStopEvent); - } - } -} \ 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 2ecf4320..258a4563 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 @@ -517,7 +517,7 @@ public class AiGateWayManager : DomainService await _aiMessageManager.CreateUserMessageAsync(userId.Value, sessionId, new MessageInputDto { - Content = sessionId is null ? "不予存储" : request.Messages?.FirstOrDefault()?.Content ?? string.Empty, + Content = "不予存储", ModelId = request.Model, TokenUsage = data.TokenUsage, }, tokenId); @@ -525,7 +525,7 @@ public class AiGateWayManager : DomainService await _aiMessageManager.CreateSystemMessageAsync(userId.Value, sessionId, new MessageInputDto { - Content = sessionId is null ? "不予存储" : data.content?.FirstOrDefault()?.text, + Content = "不予存储", ModelId = request.Model, TokenUsage = data.TokenUsage }, tokenId); @@ -602,7 +602,7 @@ public class AiGateWayManager : DomainService await _aiMessageManager.CreateUserMessageAsync(userId, sessionId, new MessageInputDto { - Content = sessionId is null ? "不予存储" : request.Messages?.LastOrDefault()?.Content ?? string.Empty, + Content = "不予存储", ModelId = request.Model, TokenUsage = tokenUsage, }, tokenId); @@ -610,7 +610,7 @@ public class AiGateWayManager : DomainService await _aiMessageManager.CreateSystemMessageAsync(userId, sessionId, new MessageInputDto { - Content = sessionId is null ? "不予存储" : backupSystemContent.ToString(), + Content = "不予存储", ModelId = request.Model, TokenUsage = tokenUsage }, tokenId); 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 eeb497e9..4c3c25ce 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 @@ -48,9 +48,7 @@ namespace Yi.Framework.AiHub.Domain #endregion #region Anthropic ChatCompletion - - services.AddKeyedTransient( - nameof(CustomOpenAIAnthropicChatCompletionsService)); + services.AddKeyedTransient( nameof(AnthropicChatCompletionsService));