diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiGateWay/Impl/ThorDeepSeek/Chats/DeepSeekChatCompletionsService.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiGateWay/Impl/ThorDeepSeek/Chats/DeepSeekChatCompletionsService.cs new file mode 100644 index 00000000..a00bc8e8 --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiGateWay/Impl/ThorDeepSeek/Chats/DeepSeekChatCompletionsService.cs @@ -0,0 +1,178 @@ +using System.Diagnostics; +using System.Net; +using System.Net.Http.Json; +using System.Text.Json; +using Microsoft.Extensions.Logging; +using Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi; +using Yi.Framework.AiHub.Domain.AiGateWay.Exceptions; +using Yi.Framework.AiHub.Domain.Shared.Dtos; + +namespace Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorDeepSeek.Chats; + +public sealed class DeepSeekChatCompletionsService(ILogger logger) + : IChatCompletionService +{ + public async IAsyncEnumerable CompleteChatStreamAsync(AiModelDescribe options, + ThorChatCompletionsRequest chatCompletionCreate, + CancellationToken cancellationToken) + { + if (string.IsNullOrWhiteSpace(options.Endpoint)) + { + options.Endpoint = "https://api.deepseek.com/v1"; + } + + using var openai = + Activity.Current?.Source.StartActivity("OpenAI 对话流式补全"); + + var response = await HttpClientFactory.GetHttpClient(options.Endpoint).HttpRequestRaw( + options?.Endpoint.TrimEnd('/') + "/chat/completions", + chatCompletionCreate, options.ApiKey); + + openai?.SetTag("Model", chatCompletionCreate.Model); + openai?.SetTag("Response", response.StatusCode.ToString()); + + if (response.StatusCode == HttpStatusCode.Unauthorized) + { + throw new UnauthorizedAccessException(); + } + + if (response.StatusCode == HttpStatusCode.PaymentRequired) + { + throw new PaymentRequiredException(); + } + + // 如果限流则抛出限流异常 + if (response.StatusCode == HttpStatusCode.TooManyRequests) + { + throw new ThorRateLimitException(); + } + + // 大于等于400的状态码都认为是异常 + if (response.StatusCode >= HttpStatusCode.BadRequest) + { + logger.LogError("OpenAI对话异常 , StatusCode: {StatusCode} ", response.StatusCode); + + throw new BusinessException("OpenAI对话异常", response.StatusCode.ToString()); + } + + 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; + while ((line = await reader.ReadLineAsync().ConfigureAwait(false)) != null) + { + line += Environment.NewLine; + + if (line.StartsWith('{')) + { + logger.LogInformation("OpenAI对话异常 , StatusCode: {StatusCode} Response: {Response}", response.StatusCode, + line); + + throw new BusinessException("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; + } + + + var result = JsonSerializer.Deserialize(line, + ThorJsonSerializer.DefaultOptions); + + var content = result?.Choices?.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 result.Choices) + { + choice.Delta.ReasoningContent = choice.Delta.Content; + //choice.Delta.Content = string.Empty; + } + } + + // first = false; + + yield return result; + } + } + + public async Task CompleteChatAsync(AiModelDescribe options, + ThorChatCompletionsRequest chatCompletionCreate, + CancellationToken cancellationToken) + { + using var openai = + Activity.Current?.Source.StartActivity("OpenAI 对话补全"); + + if (string.IsNullOrWhiteSpace(options.Endpoint)) + { + options.Endpoint = "https://api.deepseek.com/v1"; + } + + var response = await HttpClientFactory.GetHttpClient(options.Endpoint).PostJsonAsync( + options?.Endpoint.TrimEnd('/') + "/chat/completions", + chatCompletionCreate, options.ApiKey).ConfigureAwait(false); + + openai?.SetTag("Model", chatCompletionCreate.Model); + openai?.SetTag("Response", response.StatusCode.ToString()); + + if (response.StatusCode == HttpStatusCode.Unauthorized) + { + throw new BusinessException("渠道未登录,请联系管理人员", "401"); + } + + // 如果限流则抛出限流异常 + if (response.StatusCode == HttpStatusCode.TooManyRequests) + { + throw new ThorRateLimitException(); + } + + // 大于等于400的状态码都认为是异常 + if (response.StatusCode >= HttpStatusCode.BadRequest) + { + var error = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); + logger.LogError("OpenAI对话异常 , StatusCode: {StatusCode} Response: {Response}", response.StatusCode, error); + + throw new BusinessException("OpenAI对话异常", response.StatusCode.ToString()); + } + + var result = + await response.Content.ReadFromJsonAsync( + cancellationToken: cancellationToken).ConfigureAwait(false); + + return result; + } +} \ No newline at end of file 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 a19c3eda..4798c43d 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 @@ -3,6 +3,7 @@ using Volo.Abp.Domain; using Yi.Framework.AiHub.Domain.AiGateWay; using Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorAzureDatabricks.Chats; using Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorAzureOpenAI.Chats; +using Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorDeepSeek.Chats; using Yi.Framework.AiHub.Domain.Shared; using Yi.Framework.Mapster; @@ -25,6 +26,8 @@ namespace Yi.Framework.AiHub.Domain nameof(AzureOpenAiChatCompletionCompletionsService)); services.AddKeyedTransient( nameof(AzureDatabricksChatCompletionsService)); + services.AddKeyedTransient( + nameof(DeepSeekChatCompletionsService)); //ai模型特殊性兼容处理 Configure(options => @@ -36,6 +39,15 @@ namespace Yi.Framework.AiHub.Domain request.Temperature = null; } }); + options.Handles.Add(request => + { + if (request.Model.StartsWith("o3-mini") || request.Model.StartsWith("o4-mini")) + { + request.MaxCompletionTokens = request.MaxTokens; + request.MaxTokens = null; + request.Temperature = null; + } + }); }); }