From 69b84f66135af87bfdfc8a71e726d8c5354300d0 Mon Sep 17 00:00:00 2001 From: chenchun Date: Thu, 11 Dec 2025 17:16:21 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E6=88=90openai=E5=93=8D?= =?UTF-8?q?=E5=BA=94=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../OpenAi/Responses/OpenAiResponsesInput.cs | 21 +- .../OpenAi/Responses/OpenAiResponsesOutput.cs | 12 + .../Extensions/JsonElementExtensions.cs | 279 ++++++++++++++++++ .../AiGateWay/IOpenAiResponseService.cs | 5 +- .../Chats/OpenAiResponseService.cs | 4 +- .../Managers/AiGateWayManager.cs | 195 +++++------- 6 files changed, 379 insertions(+), 137 deletions(-) create mode 100644 Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Extensions/JsonElementExtensions.cs 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 index 2e17d351..8adf388a 100644 --- 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 @@ -1,4 +1,5 @@ -using System.Text.Json.Serialization; +using System.Text.Json; +using System.Text.Json.Serialization; namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi.Responses; @@ -7,14 +8,14 @@ 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("input")] public JsonElement Input { get; set; } [JsonPropertyName("max_output_tokens")] public int? MaxOutputTokens { get; set; } - [JsonPropertyName("max_tool_calls")] public dynamic? MaxToolCalls { get; set; } + [JsonPropertyName("max_tool_calls")] public JsonElement? MaxToolCalls { get; set; } [JsonPropertyName("instructions")] public string? Instructions { get; set; } - [JsonPropertyName("metadata")] public dynamic? Metadata { get; set; } + [JsonPropertyName("metadata")] public JsonElement? Metadata { get; set; } [JsonPropertyName("parallel_tool_calls")] public bool? ParallelToolCalls { get; set; } @@ -22,24 +23,24 @@ public class OpenAiResponsesInput [JsonPropertyName("previous_response_id")] public string? PreviousResponseId { get; set; } - [JsonPropertyName("prompt")] public dynamic? Prompt { get; set; } + [JsonPropertyName("prompt")] public JsonElement? 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("reasoning")] public JsonElement? 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("stream_options")] public JsonElement? 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("text")] public JsonElement? Text { get; set; } + [JsonPropertyName("tool_choice")] public JsonElement? ToolChoice { get; set; } + [JsonPropertyName("tools")] public JsonElement? 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; } 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 index 6ddcc5d6..31e6c4ca 100644 --- 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 @@ -52,6 +52,18 @@ public class OpenAiResponsesOutput public dynamic? User { get; set; } [JsonPropertyName("metadata")] public dynamic? Metadata { get; set; } + + public void SupplementalMultiplier(decimal multiplier) + { + if (this.Usage is not null) + { + this.Usage.InputTokens = + (int)Math.Round((this.Usage?.InputTokens ?? 0) * multiplier); + + this.Usage.OutputTokens = + (int)Math.Round((this.Usage?.OutputTokens ?? 0) * multiplier); + } + } } public class OpenAiResponsesUsageOutput diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Extensions/JsonElementExtensions.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Extensions/JsonElementExtensions.cs new file mode 100644 index 00000000..7c5b6e53 --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Extensions/JsonElementExtensions.cs @@ -0,0 +1,279 @@ +using System.Text.Json; + +namespace Yi.Framework.AiHub.Domain.Shared.Extensions; + +public static class JsonElementExtensions +{ + #region 路径访问 + + /// + /// 链式获取深层属性,支持对象属性和数组索引 + /// + /// + /// root.GetPath("user", "addresses", 0, "city") + /// + public static JsonElement? GetPath(this JsonElement element, params object[] path) + { + JsonElement current = element; + + foreach (var key in path) + { + switch (key) + { + case string propertyName: + if (current.ValueKind != JsonValueKind.Object || + !current.TryGetProperty(propertyName, out current)) + return null; + break; + + case int index: + if (current.ValueKind != JsonValueKind.Array || + index < 0 || index >= current.GetArrayLength()) + return null; + current = current[index]; + break; + + default: + return null; + } + } + + return current; + } + + /// + /// 安全获取对象属性 + /// + public static JsonElement? Get(this JsonElement element, string propertyName) + { + if (element.ValueKind == JsonValueKind.Object && + element.TryGetProperty(propertyName, out var value)) + return value; + return null; + } + + /// + /// 安全获取数组元素 + /// + public static JsonElement? Get(this JsonElement element, int index) + { + if (element.ValueKind == JsonValueKind.Array && + index >= 0 && index < element.GetArrayLength()) + return element[index]; + return null; + } + + /// + /// 链式安全获取对象属性 + /// + public static JsonElement? Get(this JsonElement? element, string propertyName) + => element?.Get(propertyName); + + /// + /// 链式安全获取数组元素 + /// + public static JsonElement? Get(this JsonElement? element, int index) + => element?.Get(index); + + #endregion + + #region 取值方法(带默认值) + + public static string? GetString(this JsonElement? element, string? defaultValue = null) + => element?.ValueKind == JsonValueKind.String ? element.Value.GetString() : defaultValue; + + public static int GetInt(this JsonElement? element, int defaultValue = 0) + => element?.ValueKind == JsonValueKind.Number ? element.Value.GetInt32() : defaultValue; + + public static long GetLong(this JsonElement? element, long defaultValue = 0) + => element?.ValueKind == JsonValueKind.Number ? element.Value.GetInt64() : defaultValue; + + public static double GetDouble(this JsonElement? element, double defaultValue = 0) + => element?.ValueKind == JsonValueKind.Number ? element.Value.GetDouble() : defaultValue; + + public static decimal GetDecimal(this JsonElement? element, decimal defaultValue = 0) + => element?.ValueKind == JsonValueKind.Number ? element.Value.GetDecimal() : defaultValue; + + public static bool GetBool(this JsonElement? element, bool defaultValue = false) + => element?.ValueKind is JsonValueKind.True or JsonValueKind.False + ? element.Value.GetBoolean() + : defaultValue; + + public static DateTime GetDateTime(this JsonElement? element, DateTime defaultValue = default) + => element?.ValueKind == JsonValueKind.String && element.Value.TryGetDateTime(out var dt) + ? dt + : defaultValue; + + public static Guid GetGuid(this JsonElement? element, Guid defaultValue = default) + => element?.ValueKind == JsonValueKind.String && element.Value.TryGetGuid(out var guid) + ? guid + : defaultValue; + + #endregion + + #region 可空取值方法 + + public static int? GetIntOrNull(this JsonElement? element) + => element?.ValueKind == JsonValueKind.Number ? element.Value.GetInt32() : null; + + public static long? GetLongOrNull(this JsonElement? element) + => element?.ValueKind == JsonValueKind.Number ? element.Value.GetInt64() : null; + + public static double? GetDoubleOrNull(this JsonElement? element) + => element?.ValueKind == JsonValueKind.Number ? element.Value.GetDouble() : null; + + public static decimal? GetDecimalOrNull(this JsonElement? element) + => element?.ValueKind == JsonValueKind.Number ? element.Value.GetDecimal() : null; + + public static bool? GetBoolOrNull(this JsonElement? element) + => element?.ValueKind is JsonValueKind.True or JsonValueKind.False + ? element.Value.GetBoolean() + : null; + + public static DateTime? GetDateTimeOrNull(this JsonElement? element) + => element?.ValueKind == JsonValueKind.String && element.Value.TryGetDateTime(out var dt) + ? dt + : null; + + public static Guid? GetGuidOrNull(this JsonElement? element) + => element?.ValueKind == JsonValueKind.String && element.Value.TryGetGuid(out var guid) + ? guid + : null; + + #endregion + + #region 数组操作 + + /// + /// 安全获取数组,不存在返回空数组 + /// + public static IEnumerable GetArray(this JsonElement? element) + { + if (element?.ValueKind == JsonValueKind.Array) + { + foreach (var item in element.Value.EnumerateArray()) + yield return item; + } + } + + /// + /// 获取数组长度 + /// + public static int GetArrayLength(this JsonElement? element) + => element?.ValueKind == JsonValueKind.Array ? element.Value.GetArrayLength() : 0; + + /// + /// 数组转 List + /// + public static List ToStringList(this JsonElement? element) + => element.GetArray().Select(e => e.GetString()).ToList(); + + public static List ToIntList(this JsonElement? element) + => element.GetArray() + .Where(e => e.ValueKind == JsonValueKind.Number) + .Select(e => e.GetInt32()) + .ToList(); + + #endregion + + #region 对象操作 + + /// + /// 安全枚举对象属性 + /// + public static IEnumerable GetProperties(this JsonElement? element) + { + if (element?.ValueKind == JsonValueKind.Object) + { + foreach (var prop in element.Value.EnumerateObject()) + yield return prop; + } + } + + /// + /// 获取所有属性名 + /// + public static IEnumerable GetPropertyNames(this JsonElement? element) + => element.GetProperties().Select(p => p.Name); + + /// + /// 判断是否包含某属性 + /// + public static bool HasProperty(this JsonElement? element, string propertyName) + => element?.ValueKind == JsonValueKind.Object && + element.Value.TryGetProperty(propertyName, out _); + + #endregion + + #region 类型判断 + + public static bool IsNull(this JsonElement? element) + => element == null || element.Value.ValueKind == JsonValueKind.Null; + + public static bool IsNullOrUndefined(this JsonElement? element) + => element == null || element.Value.ValueKind is JsonValueKind.Null or JsonValueKind.Undefined; + + public static bool IsObject(this JsonElement? element) + => element?.ValueKind == JsonValueKind.Object; + + public static bool IsArray(this JsonElement? element) + => element?.ValueKind == JsonValueKind.Array; + + public static bool IsString(this JsonElement? element) + => element?.ValueKind == JsonValueKind.String; + + public static bool IsNumber(this JsonElement? element) + => element?.ValueKind == JsonValueKind.Number; + + public static bool IsBool(this JsonElement? element) + => element?.ValueKind is JsonValueKind.True or JsonValueKind.False; + + public static bool Exists(this JsonElement? element) + => element != null && element.Value.ValueKind != JsonValueKind.Undefined; + + #endregion + + #region 反序列化 + + /// + /// 反序列化为指定类型 + /// + public static T? Deserialize(this JsonElement? element, JsonSerializerOptions? options = null) + => element.HasValue ? element.Value.Deserialize(options) : default; + + /// + /// 反序列化为指定类型,带默认值 + /// + public static T Deserialize(this JsonElement? element, T defaultValue, JsonSerializerOptions? options = null) + => element.HasValue ? element.Value.Deserialize(options) ?? defaultValue : defaultValue; + + #endregion + + #region 转换为字典/动态类型 + + /// + /// 转换为 Dictionary + /// + public static Dictionary? ToDictionary(this JsonElement? element) + { + if (element?.ValueKind != JsonValueKind.Object) + return null; + + var dict = new Dictionary(); + foreach (var prop in element.Value.EnumerateObject()) + dict[prop.Name] = prop.Value; + return dict; + } + + #endregion + + #region 原始值 + + /// + /// 获取原始 JSON 字符串 + /// + public static string? GetRawText(this JsonElement? element) + => element?.GetRawText(); + + #endregion +} \ 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 index ab719e86..04cc9672 100644 --- 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 @@ -1,4 +1,5 @@ -using Yi.Framework.AiHub.Domain.Shared.Dtos; +using System.Text.Json; +using Yi.Framework.AiHub.Domain.Shared.Dtos; using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi.Responses; namespace Yi.Framework.AiHub.Domain.AiGateWay; @@ -12,7 +13,7 @@ public interface IOpenAiResponseService /// /// /// - public IAsyncEnumerable<(string, dynamic?)> ResponsesStreamAsync(AiModelDescribe aiModelDescribe, + public IAsyncEnumerable<(string, JsonElement?)> ResponsesStreamAsync(AiModelDescribe aiModelDescribe, OpenAiResponsesInput input, CancellationToken cancellationToken); diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiGateWay/Impl/ThorCustomOpenAI/Chats/OpenAiResponseService.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiGateWay/Impl/ThorCustomOpenAI/Chats/OpenAiResponseService.cs index 51d248fc..975c28a1 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiGateWay/Impl/ThorCustomOpenAI/Chats/OpenAiResponseService.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiGateWay/Impl/ThorCustomOpenAI/Chats/OpenAiResponseService.cs @@ -13,7 +13,7 @@ namespace Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorCustomOpenAI.Chats; public class OpenAiResponseService(ILogger logger,IHttpClientFactory httpClientFactory):IOpenAiResponseService { - public async IAsyncEnumerable<(string, dynamic?)> ResponsesStreamAsync(AiModelDescribe options, OpenAiResponsesInput input, + public async IAsyncEnumerable<(string, JsonElement?)> ResponsesStreamAsync(AiModelDescribe options, OpenAiResponsesInput input, CancellationToken cancellationToken) { using var openai = @@ -73,7 +73,7 @@ public class OpenAiResponseService(ILogger logger,IHttpCl data = line[OpenAIConstant.Data.Length..].Trim(); - var result = JsonSerializer.Deserialize(data, + var result = JsonSerializer.Deserialize(data, ThorJsonSerializer.DefaultOptions); yield return (eventType, result); 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 d0346507..2ecf4320 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 @@ -21,6 +21,7 @@ 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.AiHub.Domain.Shared.Extensions; using Yi.Framework.Core.Extensions; using Yi.Framework.SqlSugarCore.Abstractions; using JsonSerializer = System.Text.Json.JsonSerializer; @@ -91,30 +92,7 @@ public class AiGateWayManager : DomainService return aiModelDescribe; } - - /// - /// 聊天完成-流式 - /// - /// - /// - /// - public async IAsyncEnumerable CompleteChatStreamAsync( - ThorChatCompletionsRequest request, - [EnumeratorCancellation] CancellationToken cancellationToken) - { - _specialCompatible.Compatible(request); - var modelDescribe = await GetModelAsync(ModelApiTypeEnum.OpenAi, request.Model); - var chatService = - LazyServiceProvider.GetRequiredKeyedService(modelDescribe.HandlerName); - - await foreach (var result in chatService.CompleteChatStreamAsync(modelDescribe, request, cancellationToken)) - { - result.SupplementalMultiplier(modelDescribe.Multiplier); - yield return result; - } - } - - + /// /// 聊天完成-非流式 /// @@ -176,6 +154,7 @@ public class AiGateWayManager : DomainService await response.WriteAsJsonAsync(data, cancellationToken); } + /// /// 聊天完成-缓存处理 /// @@ -201,8 +180,12 @@ public class AiGateWayManager : DomainService response.Headers.TryAdd("Connection", "keep-alive"); - var gateWay = LazyServiceProvider.GetRequiredService(); - var completeChatResponse = gateWay.CompleteChatStreamAsync(request, cancellationToken); + _specialCompatible.Compatible(request); + var modelDescribe = await GetModelAsync(ModelApiTypeEnum.OpenAi, request.Model); + var chatService = + LazyServiceProvider.GetRequiredKeyedService(modelDescribe.HandlerName); + + var completeChatResponse = chatService.CompleteChatStreamAsync(modelDescribe,request, cancellationToken); var tokenUsage = new ThorUsageResponse(); //缓存队列算法 @@ -244,6 +227,7 @@ public class AiGateWayManager : DomainService { await foreach (var data in completeChatResponse) { + data.SupplementalMultiplier(modelDescribe.Multiplier); if (data.Usage is not null && (data.Usage.CompletionTokens > 0 || data.Usage.OutputTokens > 0)) { tokenUsage = data.Usage; @@ -316,8 +300,8 @@ public class AiGateWayManager : DomainService } } } - - + + /// /// 图片生成 /// @@ -385,8 +369,8 @@ public class AiGateWayManager : DomainService throw new UserFriendlyException(errorContent); } } - - + + /// /// 向量生成 /// @@ -498,30 +482,7 @@ public class AiGateWayManager : DomainService throw new UserFriendlyException(errorContent); } } - - - /// - /// Anthropic聊天完成-流式 - /// - /// - /// - /// - public async IAsyncEnumerable<(string, AnthropicStreamDto?)> AnthropicCompleteChatStreamAsync( - AnthropicInput request, - [EnumeratorCancellation] CancellationToken cancellationToken) - { - _specialCompatible.AnthropicCompatible(request); - var modelDescribe = await GetModelAsync(ModelApiTypeEnum.Claude, request.Model); - var chatService = - LazyServiceProvider.GetRequiredKeyedService(modelDescribe.HandlerName); - - await foreach (var result in chatService.StreamChatCompletionsAsync(modelDescribe, request, cancellationToken)) - { - result.Item2.SupplementalMultiplier(modelDescribe.Multiplier); - yield return result; - } - } - + /// /// Anthropic聊天完成-非流式 @@ -582,6 +543,7 @@ public class AiGateWayManager : DomainService await response.WriteAsJsonAsync(data, cancellationToken); } + /// /// Anthropic聊天完成-缓存处理 /// @@ -605,16 +567,20 @@ public class AiGateWayManager : DomainService response.ContentType = "text/event-stream;charset=utf-8;"; response.Headers.TryAdd("Cache-Control", "no-cache"); response.Headers.TryAdd("Connection", "keep-alive"); - - - var gateWay = LazyServiceProvider.GetRequiredService(); - var completeChatResponse = gateWay.AnthropicCompleteChatStreamAsync(request, cancellationToken); + + _specialCompatible.AnthropicCompatible(request); + var modelDescribe = await GetModelAsync(ModelApiTypeEnum.Claude, request.Model); + var chatService = + LazyServiceProvider.GetRequiredKeyedService(modelDescribe.HandlerName); + + var completeChatResponse = chatService.StreamChatCompletionsAsync(modelDescribe,request, cancellationToken); ThorUsageResponse? tokenUsage = null; StringBuilder backupSystemContent = new StringBuilder(); try { await foreach (var responseResult in completeChatResponse) { + responseResult.Item2.SupplementalMultiplier(modelDescribe.Multiplier); //message_start是为了保底机制 if (responseResult.Item1.Contains("message_delta") || responseResult.Item1.Contains("message_start")) { @@ -679,7 +645,6 @@ public class AiGateWayManager : DomainService Guid? tokenId = null, CancellationToken cancellationToken = default) { - //todo 1 // _specialCompatible.AnthropicCompatible(request); var response = httpContext.Response; // 设置响应头,声明是 json @@ -689,69 +654,47 @@ public class AiGateWayManager : DomainService 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); - // } - // } + + data.SupplementalMultiplier(modelDescribe.Multiplier); + + var tokenUsage= new ThorUsageResponse + { + InputTokens = data.Usage.InputTokens, + OutputTokens = data.Usage.OutputTokens, + TotalTokens = data.Usage.InputTokens + data.Usage.OutputTokens, + }; + if (userId is not null) + { + await _aiMessageManager.CreateUserMessageAsync(userId.Value, sessionId, + new MessageInputDto + { + Content = "不予存储", + ModelId = request.Model, + TokenUsage = tokenUsage, + }, tokenId); + + await _aiMessageManager.CreateSystemMessageAsync(userId.Value, sessionId, + new MessageInputDto + { + Content = "不予存储", + ModelId = request.Model, + TokenUsage = tokenUsage + }, tokenId); + + await _usageStatisticsManager.SetUsageAsync(userId.Value, request.Model, tokenUsage, tokenId); + + // 扣减尊享token包用量 + var totalTokens = 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响应-流式-缓存处理 /// @@ -776,7 +719,11 @@ public class AiGateWayManager : DomainService response.Headers.TryAdd("Cache-Control", "no-cache"); response.Headers.TryAdd("Connection", "keep-alive"); - var completeChatResponse = OpenAiResponsesAsync(request, cancellationToken); + var modelDescribe = await GetModelAsync(ModelApiTypeEnum.Response, request.Model); + var chatService = + LazyServiceProvider.GetRequiredKeyedService(modelDescribe.HandlerName); + + var completeChatResponse = chatService.ResponsesStreamAsync(modelDescribe,request, cancellationToken); ThorUsageResponse? tokenUsage = null; try { @@ -785,12 +732,14 @@ public class AiGateWayManager : DomainService //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; + var obj = responseResult.Item2!.Value; + int inputTokens = obj.GetPath("response","usage","input_tokens").GetInt(); + int outputTokens = obj.GetPath("response","usage","output_tokens").GetInt(); + inputTokens=Convert.ToInt32(inputTokens * modelDescribe.Multiplier); + outputTokens=Convert.ToInt32(outputTokens * modelDescribe.Multiplier); tokenUsage = new ThorUsageResponse { - PromptTokens = inputTokens, + PromptTokens =inputTokens, InputTokens = inputTokens, OutputTokens = outputTokens, CompletionTokens = outputTokens, @@ -839,7 +788,7 @@ public class AiGateWayManager : DomainService } - #region Anthropic格式Http响应 + #region 流式传输格式Http响应 private static readonly byte[] EventPrefix = "event: "u8.ToArray(); private static readonly byte[] DataPrefix = "data: "u8.ToArray();