From 433d616b9b2336e28e3930964ff17cb848a6a9d7 Mon Sep 17 00:00:00 2001 From: ccnetcore Date: Thu, 11 Dec 2025 01:17:31 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81codex?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Services/OpenApiService.cs | 65 ++++++- .../OpenAi/Responses/OpenAiResponsesInput.cs | 46 +++++ .../OpenAi/Responses/OpenAiResponsesOutput.cs | 79 ++++++++ .../Enums/ModelApiTypeEnum.cs | 5 +- .../AiGateWay/IOpenAiResponseService.cs | 29 +++ ...omOpenAIAnthropicChatCompletionsService.cs | 28 +-- .../Chats/OpenAIChatCompletionsService.cs | 2 +- .../Chats/OpenAiResponseService.cs | 123 ++++++++++++ .../Managers/AiGateWayManager.cs | 184 +++++++++++++++++- .../YiFrameworkAiHubDomainModule.cs | 6 + 10 files changed, 531 insertions(+), 36 deletions(-) create mode 100644 Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Dtos/OpenAi/Responses/OpenAiResponsesInput.cs create mode 100644 Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Dtos/OpenAi/Responses/OpenAiResponsesOutput.cs create mode 100644 Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiGateWay/IOpenAiResponseService.cs create mode 100644 Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiGateWay/Impl/ThorCustomOpenAI/Chats/OpenAiResponseService.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 60145a29..0ccf376a 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 @@ -12,6 +12,7 @@ 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.Dtos.OpenAi.Responses; using Yi.Framework.AiHub.Domain.Shared.Enums; using Yi.Framework.Rbac.Application.Contracts.IServices; using Yi.Framework.SqlSugarCore.Abstractions; @@ -85,6 +86,7 @@ public class OpenApiService : ApplicationService } } + /// /// 图片生成 /// @@ -102,6 +104,7 @@ public class OpenApiService : ApplicationService await _aiGateWayManager.CreateImageForStatisticsAsync(httpContext, userId, null, input, tokenId); } + /// /// 向量生成 /// @@ -145,7 +148,7 @@ public class OpenApiService : ApplicationService }; } - + /// /// Anthropic对话(尊享服务专用) /// @@ -185,17 +188,69 @@ public class OpenApiService : ApplicationService //ai网关代理httpcontext if (input.Stream) { - await _aiGateWayManager.AnthropicCompleteChatStreamForStatisticsAsync(_httpContextAccessor.HttpContext, input, + await _aiGateWayManager.AnthropicCompleteChatStreamForStatisticsAsync(_httpContextAccessor.HttpContext, + input, userId, null, tokenId, cancellationToken); } else { - await _aiGateWayManager.AnthropicCompleteChatForStatisticsAsync(_httpContextAccessor.HttpContext, input, userId, + await _aiGateWayManager.AnthropicCompleteChatForStatisticsAsync(_httpContextAccessor.HttpContext, input, + userId, null, tokenId, cancellationToken); } } + /// + /// 响应-Openai新规范 (尊享服务专用) + /// + /// + /// + [HttpPost("openApi/v1/responses")] + public async Task ResponsesAsync([FromBody] OpenAiResponsesInput input, CancellationToken cancellationToken) + { + //前面都是校验,后面才是真正的调用 + var httpContext = this._httpContextAccessor.HttpContext; + var tokenValidation = await _tokenManager.ValidateTokenAsync(GetTokenByHttpContext(httpContext), input.Model); + var userId = tokenValidation.UserId; + var tokenId = tokenValidation.TokenId; + await _aiBlacklistManager.VerifiyAiBlacklist(userId); + + // 验证用户是否为VIP + var userInfo = await _accountService.GetAsync(null, null, userId); + if (userInfo == null) + { + throw new UserFriendlyException("用户信息不存在"); + } + + // 检查是否为VIP(使用RoleCodes判断) + if (!userInfo.RoleCodes.Contains(AiHubConst.VipRole) && userInfo.User.UserName != "cc") + { + throw new UserFriendlyException("该接口为尊享服务专用,需要VIP权限才能使用"); + } + + // 检查尊享token包用量 + var availableTokens = await _premiumPackageManager.GetAvailableTokensAsync(userId); + if (availableTokens <= 0) + { + throw new UserFriendlyException("尊享token包用量不足,请先购买尊享token包"); + } + + //ai网关代理httpcontext + if (input.Stream == true) + { + await _aiGateWayManager.OpenAiResponsesStreamForStatisticsAsync(_httpContextAccessor.HttpContext, + input, + userId, null, tokenId, cancellationToken); + } + else + { + await _aiGateWayManager.OpenAiResponsesAsyncForStatisticsAsync(_httpContextAccessor.HttpContext, input, + userId, + null, tokenId, + cancellationToken); + } + } #region 私有 @@ -210,7 +265,8 @@ public class OpenApiService : ApplicationService // 再检查 Authorization 头 string authHeader = httpContext.Request.Headers["Authorization"]; - if (!string.IsNullOrWhiteSpace(authHeader) && authHeader.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) + if (!string.IsNullOrWhiteSpace(authHeader) && + authHeader.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) { return authHeader.Substring("Bearer ".Length).Trim(); } @@ -227,5 +283,4 @@ public class OpenApiService : ApplicationService } #endregion - } \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Dtos/OpenAi/Responses/OpenAiResponsesInput.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Dtos/OpenAi/Responses/OpenAiResponsesInput.cs new file mode 100644 index 00000000..2e17d351 --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Dtos/OpenAi/Responses/OpenAiResponsesInput.cs @@ -0,0 +1,46 @@ +using System.Text.Json.Serialization; + +namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi.Responses; + +public class OpenAiResponsesInput +{ + [JsonPropertyName("stream")] public bool? Stream { get; set; } + + [JsonPropertyName("model")] public string Model { get; set; } + [JsonPropertyName("input")] public dynamic Input { get; set; } + + [JsonPropertyName("max_output_tokens")] + public int? MaxOutputTokens { get; set; } + + [JsonPropertyName("max_tool_calls")] public dynamic? MaxToolCalls { get; set; } + [JsonPropertyName("instructions")] public string? Instructions { get; set; } + [JsonPropertyName("metadata")] public dynamic? Metadata { get; set; } + + [JsonPropertyName("parallel_tool_calls")] + public bool? ParallelToolCalls { get; set; } + + [JsonPropertyName("previous_response_id")] + public string? PreviousResponseId { get; set; } + + [JsonPropertyName("prompt")] public dynamic? Prompt { get; set; } + [JsonPropertyName("prompt_cache_key")] public string? PromptCacheKey { get; set; } + + [JsonPropertyName("prompt_cache_retention")] + public string? PromptCacheRetention { get; set; } + + [JsonPropertyName("reasoning")] public dynamic? Reasoning { get; set; } + + [JsonPropertyName("safety_identifier")] + public string? SafetyIdentifier { get; set; } + + [JsonPropertyName("service_tier")] public string? ServiceTier { get; set; } + [JsonPropertyName("store")] public bool? Store { get; set; } + [JsonPropertyName("stream_options")] public dynamic? StreamOptions { get; set; } + [JsonPropertyName("temperature")] public decimal? Temperature { get; set; } + [JsonPropertyName("text")] public dynamic? Text { get; set; } + [JsonPropertyName("tool_choice")] public dynamic? ToolChoice { get; set; } + [JsonPropertyName("tools")] public dynamic? Tools { get; set; } + [JsonPropertyName("top_logprobs")] public int? TopLogprobs { get; set; } + [JsonPropertyName("top_p")] public decimal? TopP { get; set; } + [JsonPropertyName("truncation")] public string? Truncation { get; set; } +} \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Dtos/OpenAi/Responses/OpenAiResponsesOutput.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Dtos/OpenAi/Responses/OpenAiResponsesOutput.cs new file mode 100644 index 00000000..6ddcc5d6 --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Dtos/OpenAi/Responses/OpenAiResponsesOutput.cs @@ -0,0 +1,79 @@ +using System.Text.Json.Serialization; + +namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi.Responses; + +public class OpenAiResponsesOutput +{ + [JsonPropertyName("id")] + public string? Id { get; set; } + [JsonPropertyName("object")] + public string? Object { get; set; } + [JsonPropertyName("created_at")] + public long CreatedAt { get; set; } + [JsonPropertyName("status")] + public string? Status { get; set; } + [JsonPropertyName("error")] + public dynamic? Error { get; set; } + [JsonPropertyName("incomplete_details")] + public dynamic? IncompleteDetails { get; set; } + [JsonPropertyName("instructions")] + public dynamic? Instructions { get; set; } + [JsonPropertyName("max_output_tokens")] + public dynamic? MaxOutputTokens { get; set; } + [JsonPropertyName("model")] + public string? Model { get; set; } + // output 是复杂对象 + [JsonPropertyName("output")] + public List? Output { get; set; } + [JsonPropertyName("parallel_tool_calls")] + public bool ParallelToolCalls { get; set; } + [JsonPropertyName("previous_response_id")] + public dynamic? PreviousResponseId { get; set; } + [JsonPropertyName("reasoning")] + public dynamic? Reasoning { get; set; } + [JsonPropertyName("store")] + public bool Store { get; set; } + [JsonPropertyName("temperature")] + public double Temperature { get; set; } + [JsonPropertyName("text")] + public dynamic? Text { get; set; } + [JsonPropertyName("tool_choice")] + public string? ToolChoice { get; set; } + [JsonPropertyName("tools")] + public List? Tools { get; set; } + [JsonPropertyName("top_p")] + public double TopP { get; set; } + [JsonPropertyName("truncation")] + public string? Truncation { get; set; } + // usage 为唯一强类型 + [JsonPropertyName("usage")] + public OpenAiResponsesUsageOutput? Usage { get; set; } + [JsonPropertyName("user")] + public dynamic? User { get; set; } + [JsonPropertyName("metadata")] + public dynamic? Metadata { get; set; } +} + +public class OpenAiResponsesUsageOutput +{ + [JsonPropertyName("input_tokens")] + public int InputTokens { get; set; } + [JsonPropertyName("input_tokens_details")] + public OpenAiResponsesInputTokensDetails? InputTokensDetails { get; set; } + [JsonPropertyName("output_tokens")] + public int OutputTokens { get; set; } + [JsonPropertyName("output_tokens_details")] + public OpenAiResponsesOutputTokensDetails? OutputTokensDetails { get; set; } + [JsonPropertyName("total_tokens")] + public int TotalTokens { get; set; } +} +public class OpenAiResponsesInputTokensDetails +{ + [JsonPropertyName("cached_tokens")] + public int CachedTokens { get; set; } +} +public class OpenAiResponsesOutputTokensDetails +{ + [JsonPropertyName("reasoning_tokens")] + public int ReasoningTokens { get; set; } +} \ 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 index cc5cb30c..5b407f60 100644 --- 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 @@ -8,5 +8,8 @@ public enum ModelApiTypeEnum OpenAi, [Description("Claude")] - Claude + Claude, + + [Description("Response")] + Response } \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiGateWay/IOpenAiResponseService.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiGateWay/IOpenAiResponseService.cs new file mode 100644 index 00000000..ab719e86 --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiGateWay/IOpenAiResponseService.cs @@ -0,0 +1,29 @@ +using Yi.Framework.AiHub.Domain.Shared.Dtos; +using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi.Responses; + +namespace Yi.Framework.AiHub.Domain.AiGateWay; + +public interface IOpenAiResponseService +{ + /// + /// 响应-流式 + /// + /// + /// + /// + /// + public IAsyncEnumerable<(string, dynamic?)> ResponsesStreamAsync(AiModelDescribe aiModelDescribe, + OpenAiResponsesInput input, + CancellationToken cancellationToken); + + /// + /// 响应-非流式 + /// + /// + /// + /// + /// + public Task ResponsesAsync(AiModelDescribe aiModelDescribe, + OpenAiResponsesInput input, + CancellationToken cancellationToken); +} \ 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 index 04f06d66..7240e37e 100644 --- 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 @@ -26,19 +26,7 @@ public class CustomOpenAIAnthropicChatCompletionsService( { // 转换请求格式:Claude -> OpenAI var openAIRequest = AnthropicToOpenAi.ConvertAnthropicToOpenAi(request); - - if (openAIRequest.Model.StartsWith("gpt-5")) - { - openAIRequest.MaxCompletionTokens = request.MaxTokens; - openAIRequest.MaxTokens = null; - } - else if (openAIRequest.Model.StartsWith("o3-mini") || openAIRequest.Model.StartsWith("o4-mini")) - { - openAIRequest.MaxCompletionTokens = request.MaxTokens; - openAIRequest.MaxTokens = null; - openAIRequest.Temperature = null; - } - + // 调用OpenAI服务 var openAIResponse = await GetChatCompletionService().CompleteChatAsync(aiModelDescribe,openAIRequest, cancellationToken); @@ -55,19 +43,7 @@ public class CustomOpenAIAnthropicChatCompletionsService( { var openAIRequest = AnthropicToOpenAi.ConvertAnthropicToOpenAi(request); openAIRequest.Stream = true; - - if (openAIRequest.Model.StartsWith("gpt-5")) - { - openAIRequest.MaxCompletionTokens = request.MaxTokens; - openAIRequest.MaxTokens = null; - } - else if (openAIRequest.Model.StartsWith("o3-mini") || openAIRequest.Model.StartsWith("o4-mini")) - { - openAIRequest.MaxCompletionTokens = request.MaxTokens; - openAIRequest.MaxTokens = null; - openAIRequest.Temperature = null; - } - + var messageId = Guid.NewGuid().ToString(); var hasStarted = false; var hasTextContentBlockStarted = false; diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiGateWay/Impl/ThorCustomOpenAI/Chats/OpenAIChatCompletionsService.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiGateWay/Impl/ThorCustomOpenAI/Chats/OpenAIChatCompletionsService.cs index 32094c8a..1598d0ff 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiGateWay/Impl/ThorCustomOpenAI/Chats/OpenAIChatCompletionsService.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiGateWay/Impl/ThorCustomOpenAI/Chats/OpenAIChatCompletionsService.cs @@ -130,7 +130,7 @@ public sealed class OpenAiChatCompletionsService(ILogger logger,IHttpClientFactory httpClientFactory):IOpenAiResponseService +{ + + public async IAsyncEnumerable<(string, dynamic?)> ResponsesStreamAsync(AiModelDescribe options, OpenAiResponsesInput input, + CancellationToken cancellationToken) + { + using var openai = + Activity.Current?.Source.StartActivity("OpenAi 响应"); + + + var client = httpClientFactory.CreateClient(); + + var response = await client.HttpRequestRaw(options.Endpoint.TrimEnd('/') + "/responses", input, options.ApiKey); + + 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; + + string? data = null; + string eventType = string.Empty; + + 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 (string.IsNullOrWhiteSpace(line)) + { + continue; + } + + if (line.StartsWith("event:")) + { + eventType = line; + continue; + } + + if (!line.StartsWith(OpenAIConstant.Data)) continue; + + data = line[OpenAIConstant.Data.Length..].Trim(); + + var result = JsonSerializer.Deserialize(data, + ThorJsonSerializer.DefaultOptions); + + yield return (eventType, result); + } + } + + public async Task ResponsesAsync(AiModelDescribe options, OpenAiResponsesInput chatCompletionCreate, + CancellationToken cancellationToken) + { + using var openai = + Activity.Current?.Source.StartActivity("OpenAI 响应"); + + var response = await httpClientFactory.CreateClient().PostJsonAsync( + options?.Endpoint.TrimEnd('/') + "/responses", + 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 响应异常 请求地址:{Address}, StatusCode: {StatusCode} Response: {Response}", options.Endpoint, + 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/Managers/AiGateWayManager.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/AiGateWayManager.cs index 4e2d3645..d0346507 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 @@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using Newtonsoft.Json.Serialization; using Volo.Abp.Domain.Services; using Yi.Framework.AiHub.Domain.AiGateWay; @@ -18,6 +19,7 @@ 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.Dtos.OpenAi.Responses; using Yi.Framework.AiHub.Domain.Shared.Enums; using Yi.Framework.Core.Extensions; using Yi.Framework.SqlSugarCore.Abstractions; @@ -546,9 +548,9 @@ public class AiGateWayManager : DomainService var chatService = LazyServiceProvider.GetRequiredKeyedService(modelDescribe.HandlerName); var data = await chatService.ChatCompletionsAsync(modelDescribe, request, cancellationToken); - + data.SupplementalMultiplier(modelDescribe.Multiplier); - + if (userId is not null) { await _aiMessageManager.CreateUserMessageAsync(userId.Value, sessionId, @@ -660,6 +662,183 @@ public class AiGateWayManager : DomainService } } + + /// + /// OpenAi 响应-非流式-缓存处理 + /// + /// + /// + /// + /// + /// + /// + public async Task OpenAiResponsesAsyncForStatisticsAsync(HttpContext httpContext, + OpenAiResponsesInput request, + Guid? userId = null, + Guid? sessionId = null, + Guid? tokenId = null, + CancellationToken cancellationToken = default) + { + //todo 1 + // _specialCompatible.AnthropicCompatible(request); + var response = httpContext.Response; + // 设置响应头,声明是 json + //response.ContentType = "application/json; charset=UTF-8"; + var modelDescribe = await GetModelAsync(ModelApiTypeEnum.Response, request.Model); + + var chatService = + LazyServiceProvider.GetRequiredKeyedService(modelDescribe.HandlerName); + var data = await chatService.ResponsesAsync(modelDescribe, request, cancellationToken); + + //todo 2 + // data.SupplementalMultiplier(modelDescribe.Multiplier); + + //todo 3 + + // if (userId is not null) + // { + // await _aiMessageManager.CreateUserMessageAsync(userId.Value, sessionId, + // new MessageInputDto + // { + // Content = sessionId is null ? "不予存储" : request.Messages?.FirstOrDefault()?.Content ?? string.Empty, + // ModelId = request.Model, + // TokenUsage = data.TokenUsage, + // }, tokenId); + // + // await _aiMessageManager.CreateSystemMessageAsync(userId.Value, sessionId, + // new MessageInputDto + // { + // Content = sessionId is null ? "不予存储" : data.content?.FirstOrDefault()?.text, + // ModelId = request.Model, + // TokenUsage = data.TokenUsage + // }, tokenId); + // + // await _usageStatisticsManager.SetUsageAsync(userId.Value, request.Model, data.TokenUsage, tokenId); + // + // // 扣减尊享token包用量 + // var totalTokens = data.TokenUsage.TotalTokens ?? 0; + // if (totalTokens > 0) + // { + // await PremiumPackageManager.TryConsumeTokensAsync(userId.Value, totalTokens); + // } + // } + + await response.WriteAsJsonAsync(data, cancellationToken); + } + + + /// + /// OpenAi 响应-流式 + /// + /// + /// + /// + public async IAsyncEnumerable<(string, dynamic?)> OpenAiResponsesAsync( + OpenAiResponsesInput request, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + //todo cc + // _specialCompatible.AnthropicCompatible(request); + var modelDescribe = await GetModelAsync(ModelApiTypeEnum.Response, request.Model); + var chatService = + LazyServiceProvider.GetRequiredKeyedService(modelDescribe.HandlerName); + + await foreach (var result in chatService.ResponsesStreamAsync(modelDescribe, request, cancellationToken)) + { + //todo 倍率 + // result.Item2.SupplementalMultiplier(modelDescribe.Multiplier); + yield return result; + } + } + + + /// + /// OpenAi响应-流式-缓存处理 + /// + /// + /// + /// + /// + /// Token Id(Web端传null或Guid.Empty) + /// + /// + public async Task OpenAiResponsesStreamForStatisticsAsync( + HttpContext httpContext, + OpenAiResponsesInput request, + Guid? userId = null, + Guid? sessionId = null, + Guid? tokenId = null, + CancellationToken cancellationToken = default) + { + var response = httpContext.Response; + // 设置响应头,声明是 SSE 流 + response.ContentType = "text/event-stream;charset=utf-8;"; + response.Headers.TryAdd("Cache-Control", "no-cache"); + response.Headers.TryAdd("Connection", "keep-alive"); + + var completeChatResponse = OpenAiResponsesAsync(request, cancellationToken); + ThorUsageResponse? tokenUsage = null; + try + { + await foreach (var responseResult in completeChatResponse) + { + //message_start是为了保底机制 + if (responseResult.Item1.Contains("response.completed")) + { + JObject obj = JObject.FromObject(responseResult.Item2); + int inputTokens = (int?)obj["response"]?["usage"]?["input_tokens"] ?? 0; + int outputTokens = (int?)obj["response"]?["usage"]?["output_tokens"] ?? 0; + tokenUsage = new ThorUsageResponse + { + PromptTokens = inputTokens, + InputTokens = inputTokens, + OutputTokens = outputTokens, + CompletionTokens = outputTokens, + TotalTokens = inputTokens+outputTokens, + }; + } + + await WriteAsEventStreamDataAsync(httpContext, responseResult.Item1, responseResult.Item2, + cancellationToken); + } + } + catch (Exception e) + { + _logger.LogError(e, $"Ai响应异常"); + var errorContent = $"响应Ai异常,异常信息:\n当前Ai模型:{request.Model}\n异常信息:{e.Message}\n异常堆栈:{e}"; + throw new UserFriendlyException(errorContent); + } + + await _aiMessageManager.CreateUserMessageAsync(userId, sessionId, + new MessageInputDto + { + Content = "不予存储" , + ModelId = request.Model, + TokenUsage = tokenUsage, + }, tokenId); + + await _aiMessageManager.CreateSystemMessageAsync(userId, sessionId, + new MessageInputDto + { + Content = "不予存储" , + ModelId = request.Model, + TokenUsage = tokenUsage + }, tokenId); + + await _usageStatisticsManager.SetUsageAsync(userId, request.Model, tokenUsage, tokenId); + + // 扣减尊享token包用量 + if (userId.HasValue && tokenUsage is not null) + { + var totalTokens = tokenUsage.TotalTokens ?? 0; + if (tokenUsage.TotalTokens > 0) + { + await PremiumPackageManager.TryConsumeTokensAsync(userId.Value, totalTokens); + } + } + } + + #region Anthropic格式Http响应 private static readonly byte[] EventPrefix = "event: "u8.ToArray(); @@ -675,7 +854,6 @@ public class AiGateWayManager : DomainService string @event, T value, CancellationToken cancellationToken = default) - where T : class { var response = context.Response; var bodyStream = response.Body; 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 823643da..eeb497e9 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 @@ -56,6 +56,12 @@ namespace Yi.Framework.AiHub.Domain #endregion + #region OpenAi Response + + services.AddKeyedTransient( + nameof(OpenAiResponseService)); + + #endregion #region Image