diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/MessageInputDto.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/MessageInputDto.cs index 2ce6c8fb..e98a28af 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/MessageInputDto.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/MessageInputDto.cs @@ -1,4 +1,5 @@ using SqlSugar; +using Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi; using Yi.Framework.AiHub.Domain.Shared.Dtos; namespace Yi.Framework.AiHub.Application.Contracts.Dtos; @@ -10,5 +11,5 @@ public class MessageInputDto public string ModelId { get; set; } public string? Remark { get; set; } - public TokenUsage? TokenUsage { get; set; } + public ThorUsageResponse? TokenUsage { get; set; } } \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ChatCompletionsInput.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ChatCompletionsInput.cs deleted file mode 100644 index d5fb3f0c..00000000 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ChatCompletionsInput.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System.Collections; -using System.Text.Json; - -namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAiDto; - -public class ChatCompletionsInput -{ - public List Messages { get; set; } - - public bool? Stream { get; set; } - - public string? Prompt { get; set; } - public string Model { get; set; } - - public decimal? Temperature { get; set; } - - public int? max_tokens { get; set; } -} - -public class OpenAiMessage -{ - public string? Role { get; set; } - public object? Content { get; set; } - - public string ConvertContent() - { - if (Content is string content) - { - return content; - } - - if (Content is JsonElement jsonElement && jsonElement.ValueKind == JsonValueKind.Array) - { - var contentItems = jsonElement.Deserialize>(); - var currentContentItem = contentItems.FirstOrDefault(); - if (currentContentItem.type == "text") - { - return currentContentItem.text; - } - } - - throw new UserFriendlyException("当前格式暂不支持"); - } -} - -public class ContentItem -{ - public string? type { get; set; } - public string? text { get; set; } -} \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ChatCompletionsOutput.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ChatCompletionsOutput.cs deleted file mode 100644 index 808bbc91..00000000 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ChatCompletionsOutput.cs +++ /dev/null @@ -1,115 +0,0 @@ -using Newtonsoft.Json; - -namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi; - -public class ChatCompletionsOutput -{ - [JsonProperty("id")] - public string Id { get; set; } - [JsonProperty("object")] - public string Object { get; set; } - [JsonProperty("created_at")] - public long CreatedAt { get; set; } - [JsonProperty("status")] - public string Status { get; set; } - [JsonProperty("error")] - public object Error { get; set; } - [JsonProperty("incomplete_details")] - public object IncompleteDetails { get; set; } - [JsonProperty("instructions")] - public object Instructions { get; set; } - [JsonProperty("max_output_tokens")] - public object MaxOutputTokens { get; set; } - [JsonProperty("model")] - public string Model { get; set; } - [JsonProperty("output")] - public List Output { get; set; } - [JsonProperty("parallel_tool_calls")] - public bool ParallelToolCalls { get; set; } - [JsonProperty("previous_response_id")] - public object PreviousResponseId { get; set; } - [JsonProperty("reasoning")] - public Reasoning Reasoning { get; set; } - [JsonProperty("store")] - public bool Store { get; set; } - [JsonProperty("temperature")] - public double Temperature { get; set; } - [JsonProperty("text")] - public Text Text { get; set; } - [JsonProperty("tool_choice")] - public string ToolChoice { get; set; } - [JsonProperty("tools")] - public List Tools { get; set; } - [JsonProperty("top_p")] - public double TopP { get; set; } - [JsonProperty("truncation")] - public string Truncation { get; set; } - [JsonProperty("usage")] - public Usage Usage { get; set; } - [JsonProperty("user")] - public object User { get; set; } - [JsonProperty("metadata")] - public Dictionary Metadata { get; set; } -} -public class Output -{ - [JsonProperty("type")] - public string Type { get; set; } - [JsonProperty("id")] - public string Id { get; set; } - [JsonProperty("status")] - public string Status { get; set; } - [JsonProperty("role")] - public string Role { get; set; } - [JsonProperty("content")] - public List Content { get; set; } -} -public class Content -{ - [JsonProperty("type")] - public string Type { get; set; } - [JsonProperty("text")] - public string Text { get; set; } - [JsonProperty("annotations")] - public List Annotations { get; set; } -} -public class Reasoning -{ - [JsonProperty("effort")] - public object Effort { get; set; } - [JsonProperty("summary")] - public object Summary { get; set; } -} -public class Text -{ - [JsonProperty("format")] - public Format Format { get; set; } -} -public class Format -{ - [JsonProperty("type")] - public string Type { get; set; } -} -public class Usage -{ - [JsonProperty("input_tokens")] - public int InputTokens { get; set; } - [JsonProperty("input_tokens_details")] - public InputTokensDetails InputTokensDetails { get; set; } - [JsonProperty("output_tokens")] - public int OutputTokens { get; set; } - [JsonProperty("output_tokens_details")] - public OutputTokensDetails OutputTokensDetails { get; set; } - [JsonProperty("total_tokens")] - public int TotalTokens { get; set; } -} -public class InputTokensDetails -{ - [JsonProperty("cached_tokens")] - public int CachedTokens { get; set; } -} -public class OutputTokensDetails -{ - [JsonProperty("reasoning_tokens")] - public int ReasoningTokens { get; set; } -} diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ModelGetOutput.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ModelGetOutput.cs deleted file mode 100644 index 9c82310e..00000000 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ModelGetOutput.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAiDto; - -public class ModelGetOutput -{ - public List Data { get; set; } -} - -public class ModelDataOutput -{ - public string ModelId { get; set; } - public string Object { get; set; } - public string Owned_by { get; set; } - public List Permission { get; set; } -} \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ModelsListDto.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ModelsListDto.cs new file mode 100644 index 00000000..32f201ae --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ModelsListDto.cs @@ -0,0 +1,28 @@ +using System.Text.Json.Serialization; + +namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi; + +public class ModelsListDto +{ + [JsonPropertyName("object")] public string @object { get; set; } + + [JsonPropertyName("data")] public List Data { get; set; } + + public ModelsListDto() + { + Data = new(); + } +} + +public class ModelsDataDto +{ + [JsonPropertyName("id")] public string Id { get; set; } + + [JsonPropertyName("object")] public string @object { get; set; } + + [JsonPropertyName("created")] public long Created { get; set; } + + [JsonPropertyName("owned_by")] public string OwnedBy { get; set; } + + [JsonPropertyName("type")] public string Type { get; set; } +} \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/OpenAIConstant.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/OpenAIConstant.cs new file mode 100644 index 00000000..9920ec95 --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/OpenAIConstant.cs @@ -0,0 +1,28 @@ +namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi; + +/// +/// OpenAI常量 +/// +public static class OpenAIConstant +{ + /// + /// 字符串utf-8编码 + /// + /// + public const string Done = "[DONE]"; + + /// + /// Data: 协议头 + /// + public const string Data = "data:"; + + /// + /// think: 协议头 + /// + public const string ThinkStart = ""; + + /// + /// think: 协议尾 + /// + public const string ThinkEnd = ""; +} \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ThorChatAudioRequest.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ThorChatAudioRequest.cs new file mode 100644 index 00000000..86127c0c --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ThorChatAudioRequest.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi; + +public sealed class ThorChatAudioRequest +{ + [JsonPropertyName("voice")] + public string? Voice { get; set; } + + [JsonPropertyName("format")] + public string? Format { get; set; } +} \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ThorChatChoiceResponse.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ThorChatChoiceResponse.cs new file mode 100644 index 00000000..da93cef6 --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ThorChatChoiceResponse.cs @@ -0,0 +1,66 @@ +using System.Text.Json.Serialization; + +namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi; + +/// +/// 聊天完成选项列 +/// +public record ThorChatChoiceResponse +{ + /// + /// 模型生成的聊天完成消息。【流式】模型响应生成的聊天完成增量存储在此属性。
+ /// 在当前模型中,无论流式还是非流式,Message 和 Delta存储相同的值 + ///
+ [JsonPropertyName("delta")] + public ThorChatMessage Delta + { + get => Message; + set => Message = value; + } + + /// + /// 模型生成的聊天完成消息。【非流式】返回的消息存储在此属性。
+ /// 在当前模型中,无论流式还是非流式,Message 和 Delta存储相同的值 + ///
+ [JsonPropertyName("message")] + public ThorChatMessage Message { get; set; } + + /// + /// 选项列表中选项的索引。 + /// + [JsonPropertyName("index")] + public int? Index { get; set; } + + /// + /// 用于处理请求的服务层。仅当在请求中指定了 service_tier 参数时,才包含此字段。 + /// + [JsonPropertyName("service_tier")] + public string? ServiceTier { get; set; } + + /// + /// 模型停止生成令牌的原因。 + /// stop 如果模型达到自然停止点或提供的停止序列, + /// length 如果达到请求中指定的最大标记数, + /// content_filter 如果由于内容过滤器中的标志而省略了内容, + /// tool_calls 如果模型调用了工具,或者 function_call (已弃用) + /// 如果模型调用了函数,则会出现这种情况。 + /// + [JsonPropertyName("finish_reason")] + public string? FinishReason { get; set; } + + /// + /// 此指纹表示模型运行时使用的后端配置。 + /// 可以与 seed 请求参数结合使用,以了解何时进行了可能影响确定性的后端更改。 + /// + [JsonPropertyName("finish_details")] + public FinishDetailsResponse? FinishDetails { get; set; } + + /// + /// + /// + public class FinishDetailsResponse + { + [JsonPropertyName("type")] public string Type { get; set; } + [JsonPropertyName("stop")] public string Stop { get; set; } + } +} \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ThorChatClaudeThinking.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ThorChatClaudeThinking.cs new file mode 100644 index 00000000..e0bbc99b --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ThorChatClaudeThinking.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi; + +public class ThorChatClaudeThinking +{ + [JsonPropertyName("type")] + public string? Type { get; set; } + + [JsonPropertyName("budget_tokens")] + public int? BudgetToken { get; set; } +} \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ThorChatCompletionsRequest.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ThorChatCompletionsRequest.cs new file mode 100644 index 00000000..33891678 --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ThorChatCompletionsRequest.cs @@ -0,0 +1,289 @@ +using System.ComponentModel.DataAnnotations; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi; + +/// +/// 对话补全请求参数对象 +/// +public class ThorChatCompletionsRequest +{ + public ThorChatCompletionsRequest() + { + Messages = new List(); + } + + [JsonPropertyName("store")] + public bool? Store { get; set; } + + /// + /// 表示对话中支持的模态类型数组。可以为 null。 + /// + [JsonPropertyName("modalities")] + public string[]? Modalities { get; set; } + + /// + /// 表示对话中的音频请求参数。可以为 null。 + /// + [JsonPropertyName("audio")] public ThorChatAudioRequest? Audio { get; set; } + + /// + /// 包含迄今为止对话的消息列表 + /// + [JsonPropertyName("messages")] + public List Messages { get; set; } + + /// + /// 模型唯一编码值,如 gpt-4,gpt-3.5-turbo,moonshot-v1-8k,看底层具体平台定义 + /// + [JsonPropertyName("model")] + public string Model { get; set; } + + /// + /// 温度采样的替代方法称为核采样,介于 0 和 1 之间,其中模型考虑具有 top_p 概率质量的标记的结果。 + /// 因此 0.1 意味着仅考虑包含前 10% 概率质量的标记。 + /// 我们通常建议更改此项或 temperature ,但不要同时更改两者。 + /// 默认 1 + /// + [JsonPropertyName("top_p")] + public float? TopP { get; set; } + + /// + /// 使用什么采样温度,介于 0 和 2 之间。 + /// 较高的值(如 0.8)将使输出更加随机,而较低的值(如 0.2)将使其更加集中和确定性。 + /// 我们通常建议更改此项或 top_p ,但不要同时更改两者。 + /// 默认 1 + /// + [JsonPropertyName("temperature")] + public float? Temperature { get; set; } + + /// + /// 为每条输入消息生成多少个结果 + /// + /// 默认为 1,不得大于 5。特别的,当 temperature 非常小靠近 0 的时候, + /// 我们只能返回 1 个结果,如果这个时候 n 已经设置并且 > 1, + /// 我们的服务会返回不合法的输入参数(invalid_request_error) + /// + /// + [JsonPropertyName("n")] + public int? N { get; set; } + + /// + /// 如果设置,将发送部分消息增量,就像在 ChatGPT 中一样。 + /// 令牌可用时将作为仅数据服务器发送事件发送,流由 data: [DONE] 消息终止。 + /// + [JsonPropertyName("stream")] + public bool? Stream { get; set; } + + /// + /// 流响应选项。仅当您设置 stream: true 时才设置此项。 + /// + [JsonPropertyName("stream_options")] + public ThorStreamOptions? StreamOptions { get; set; } + + /// + /// 停止词,当全匹配这个(组)词后会停止输出,这个(组)词本身不会输出。 + /// 最多不能超过 5 个字符串,每个字符串不得超过 32 字节, + /// 默认 null + /// + [JsonIgnore] + public string? Stop { get; set; } + + /// + /// 停止词,当全匹配这个(组)词后会停止输出,这个(组)词本身不会输出。 + /// 最多不能超过 5 个字符串,每个字符串不得超过 32 字节, + /// 默认 null + /// + [JsonIgnore] + public IList? StopAsList { get; set; } + + /// + /// 停止词,当全匹配这个(组)词后会停止输出,这个(组)词本身不会输出。 + /// 最多不能超过 5 个字符串,每个字符串不得超过 32 字节, + /// 默认 null + /// + [JsonPropertyName("stop")] + public IList? StopCalculated + { + get + { + if (Stop is not null && StopAsList is not null) + { + throw new ValidationException( + "Stop 和 StopAsList 不能同时有值,其中一个应该为 null"); + } + + if (Stop is not null) + { + return new List { Stop }; + } + + return StopAsList; + } + } + + /// + /// 生成的答案允许的最大令牌数。默认情况下,模型可以返回的令牌数量为(4096个提示令牌)。 + /// + /// + [JsonPropertyName("max_tokens")] + public int? MaxTokens { get; set; } + + /// + /// 可为补全生成的令牌数量的上限,包括可见输出令牌和推理令牌。 + /// + [JsonPropertyName("max_completion_tokens")] + public int? MaxCompletionTokens { get; set; } + + /// + /// 存在惩罚,介于 -2.0 到 2.0 之间的数字。 + /// 正值会根据新生成的词汇是否出现在文本中来进行惩罚,增加模型讨论新话题的可能性, + /// 默认为 0 + /// + /// + [JsonPropertyName("presence_penalty")] + public float? PresencePenalty { get; set; } + + + /// + /// 频率惩罚,介于-2.0到2.0之间的数字。 + /// 正值会根据新生成的词汇在文本中现有的频率来进行惩罚,减少模型一字不差重复同样话语的可能性. + /// 默认为 0 + /// + /// + [JsonPropertyName("frequency_penalty")] + public float? FrequencyPenalty { get; set; } + + /// + /// 接受一个 JSON 对象,该对象将标记(由标记生成器中的标记 ID 指定)映射到从 -100 到 100 的关联偏差值。 + /// 从数学上讲,偏差会在采样之前添加到模型生成的 logits 中。 + /// 每个模型的确切效果会有所不同,但 -1 和 1 之间的值应该会降低或增加选择的可能性; + /// 像 -100 或 100 这样的值应该会导致相关令牌的禁止或独占选择。 + /// + /// + [JsonPropertyName("logit_bias")] + public object? LogitBias { get; set; } + + /// + /// 是否返回输出标记的对数概率。如果为 true,则返回 message 的 content 中返回的每个输出标记的对数概率。 + /// + [JsonPropertyName("logprobs")] + public bool? Logprobs { get; set; } + + /// + /// 0 到 20 之间的整数,指定每个标记位置最有可能返回的标记数量,每个标记都有关联的对数概率。 + /// 如果使用此参数, logprobs 必须设置为 true 。 + /// + [JsonPropertyName("top_logprobs")] + public int? TopLogprobs { get; set; } + + /// + /// 指定用于处理请求的延迟层。此参数与订阅规模层服务的客户相关: + /// 如果设置为“auto”,系统将使用规模等级积分,直至用完。 + /// 如果设置为“default”,则将使用具有较低正常运行时间 SLA 且无延迟保证的默认服务层来处理请求。 + /// 默认null + /// + [JsonPropertyName("service_tier")] + public string? ServiceTier { get; set; } + + /// + /// 模型可能调用的工具列表。目前,仅支持函数作为工具。使用它来提供模型可以为其生成 JSON 输入的函数列表。最多支持 128 个功能。 + /// + [JsonPropertyName("tools")] + public List? Tools { get; set; } + + + /// + /// 控制模型调用哪个(如果有)工具。 + /// none 表示模型不会调用任何工具,而是生成一条消息。 + /// auto 表示模型可以在生成消息或调用一个或多个工具之间进行选择。 + /// required 表示模型必须调用一个或多个工具。 + /// 通过 {"type": "function", "function": {"name": "my_function"}} 指定特定工具会强制模型调用该工具。 + /// 当不存在任何工具时, none 是默认值。如果存在工具,则 auto 是默认值。 + /// + [JsonIgnore] + public ThorToolChoice? ToolChoice { get; set; } + + [JsonPropertyName("tool_choice")] + public object? ToolChoiceCalculated + { + get + { + if (ToolChoice != null && + ToolChoice.Type != ThorToolChoiceTypeConst.Function && + ToolChoice.Function != null) + { + throw new ValidationException( + "当 type 为 \"function\" 时,属性 Function 不可为null。"); + } + + if (ToolChoice?.Type == ThorToolChoiceTypeConst.Function) + { + return ToolChoice; + } + + return ToolChoice?.Type; + } + set + { + if (value is JsonElement jsonElement) + { + if (jsonElement.ValueKind == JsonValueKind.String) + { + ToolChoice = new ThorToolChoice + { + Type = jsonElement.GetString() + }; + } + else if (jsonElement.ValueKind == JsonValueKind.Object) + { + ToolChoice = jsonElement.Deserialize(); + } + } + else + { + ToolChoice = (ThorToolChoice)value; + } + } + } + + /// + /// 设置为 {"type": "json_object"} 可启用 JSON 模式,从而保证模型生成的信息是有效的 JSON。 + /// 当你将 response_format 设置为 {"type": "json_object"} 时, + /// 你需要在 prompt 中明确地引导模型输出 JSON 格式的内容, + /// 并告知模型该 JSON 的具体格式,否则将可能导致不符合预期的结果。 + /// 默认为 {"type": "text"} + /// + [JsonPropertyName("response_format")] + public ThorResponseFormat? ResponseFormat { get; set; } + + [JsonPropertyName("metadata")] public Dictionary Metadata { get; set; } + + /// + /// 此功能处于测试阶段。 + /// 如果指定,我们的系统将尽最大努力进行确定性采样, + /// 以便具有相同 seed 和参数的重复请求应返回相同的结果。 + /// 不保证确定性,您应该参考 system_fingerprint 响应参数来监控后端的变化。 + /// + [JsonPropertyName("seed")] + public int? Seed { get; set; } + + /// + /// 代表您的最终用户的唯一标识符,可以帮助 OpenAI 监控和检测滥用行为。 + /// + [JsonPropertyName("user")] + public string User { get; set; } + + [JsonPropertyName("thinking")] public ThorChatClaudeThinking Thinking { get; set; } + + /// + /// 参数验证 + /// + /// + /// + public IEnumerable Validate() + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ThorChatCompletionsResponse.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ThorChatCompletionsResponse.cs new file mode 100644 index 00000000..e458bd62 --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ThorChatCompletionsResponse.cs @@ -0,0 +1,63 @@ +using System.Text.Json.Serialization; + +namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi; + +/// +/// 对话补全服务返回结果 +/// +public record ThorChatCompletionsResponse +{ + /// + /// 对话补全的唯一标识符。
+ /// 聊天完成的唯一标识符。如果是流式对话,每个区块都具有相同的 ID。 + ///
+ [JsonPropertyName("id")] + public string Id { get; set; } + + /// + /// 用于对话补全的模型。 + /// + [JsonPropertyName("model")] + public string? Model { get; set; } + + /// + /// 对象类型
+ /// 非流式对话补全始终为 chat.completion
+ /// 流式对话补全始终为 chat.completion.chunk
+ ///
+ [JsonPropertyName("object")] + public string? ObjectTypeName { get; set; } + + /// + /// 对话补全选项列表。如果 n 大于 1,则可以是多个。 + /// + [JsonPropertyName("choices")] + public List? Choices { get; set; } + + /// + /// 完成请求的使用情况统计信息。 + /// 仅在您 stream_options: {"include_usage": true} 设置请求时才会显示。 + /// 如果存在,则它包含一个 null 值,但最后一个块除外,该块包含整个请求的令牌使用情况统计信息。 + /// + [JsonPropertyName("usage")] + public ThorUsageResponse? Usage { get; set; } + + /// + /// 创建对话补全时的 Unix 时间戳(以秒为单位)。 + /// + [JsonPropertyName("created")] + public int Created { get; set; } + + /// + /// 此指纹表示模型运行时使用的后端配置。 + /// 可以与 seed 请求参数结合使用,以了解何时进行了可能影响确定性的后端更改。 + /// + [JsonPropertyName("system_fingerprint")] + public string SystemFingerPrint { get; set; } + + /// + /// 错误信息 + /// + [JsonPropertyName("error")] + public ThorError? Error { get; set; } +} \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ThorChatMessage.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ThorChatMessage.cs new file mode 100644 index 00000000..7ec811e2 --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ThorChatMessage.cs @@ -0,0 +1,193 @@ +using System.ComponentModel.DataAnnotations; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi; + +/// +/// 聊天消息体,建议使用CreeateXXX系列方法构建内容 +/// +public class ThorChatMessage +{ + /// + /// + /// + public ThorChatMessage() + { + + } + + /// + /// 【必填】发出消息的角色,请使用赋值,如:ThorChatMessageRoleConst.User + /// + [JsonPropertyName("role")] + public string Role { get; set; } + + /// + /// 发出的消息内容,如:你好 + /// + [JsonIgnore] + public string? Content { get; set; } + + /// + /// 发出的消息内容,仅当使用 gpt-4o 模型时才支持图像输入。 + /// + /// + /// 示例数据: + /// "content": [ + /// { + /// "type": "text", + /// "text": "What'\''s in this image?" + /// }, + /// { + /// "type": "image_url", + /// "image_url": { + /// "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg" + /// } + /// } + /// ] + /// + [JsonIgnore] + public IList? Contents { get; set; } + + /// + /// 发出的消息内容计算,用于json序列号和反序列化,Content 和 Contents 不能同时赋值,只能二选一 + /// + [JsonPropertyName("content")] + public object ContentCalculated + { + get + { + if (Content is not null && Contents is not null) + { + throw new ValidationException("Messages 中 Content 和 Contents 字段不能同时有值"); + } + + if (Content is not null) + { + return Content; + } + + return Contents!; + } + set + { + if (value is JsonElement str) + { + if (str.ValueKind == JsonValueKind.String) + { + Content = value?.ToString(); + } + else if (str.ValueKind == JsonValueKind.Array) + { + Contents = JsonSerializer.Deserialize>(value?.ToString()); + } + } + else + { + Content = value?.ToString(); + } + + } + } + + /// + /// 【可选】参与者的可选名称。提供模型信息以区分相同角色的参与者。 + /// + [JsonPropertyName("name")] + public string? Name { get; set; } + + /// + /// 工具调用 ID,此消息正在响应的工具调用。 + /// + [JsonPropertyName("tool_call_id")] + public string? ToolCallId { get; set; } + + /// + /// 函数调用,已过期,不要使用,请使用 ToolCalls + /// + [JsonPropertyName("function_call")] + public ThorChatMessageFunction? FunctionCall { get; set; } + + /// + /// 【可选】推理内容 + /// + [JsonPropertyName("reasoning_content")] + public string? ReasoningContent { get; set; } + + [JsonPropertyName("id")] + public string? Id { get; set; } + + /// + /// 工具调用列表,模型生成的工具调用,例如函数调用。
+ /// 此属性存储在客户端进行tool use 第一次调用模型返回的使用的函数名和传入的参数 + ///
+ [JsonPropertyName("tool_calls")] + public List? ToolCalls { get; set; } + + /// + /// 创建系统消息 + /// + /// 系统消息内容 + /// 参与者的可选名称。提供模型信息以区分同一角色的参与者。 + /// + public static ThorChatMessage CreateSystemMessage(string content, string? name = null) + { + return new() + { + Role = ThorChatMessageRoleConst.System, + Content = content, + Name = name + }; + } + + /// + /// 创建用户消息 + /// + /// 系统消息内容 + /// 参与者的可选名称。提供模型信息以区分同一角色的参与者。 + /// + public static ThorChatMessage CreateUserMessage(string content, string? name = null) + { + return new() + { + Role = ThorChatMessageRoleConst.User, + Content = content, + Name = name + }; + } + + /// + /// 创建助手消息 + /// + /// 系统消息内容 + /// 参与者的可选名称。提供模型信息以区分同一角色的参与者。 + /// 工具调用参数列表 + /// + public static ThorChatMessage CreateAssistantMessage(string content, string? name = null, List toolCalls = null) + { + return new() + { + Role = ThorChatMessageRoleConst.Assistant, + Content = content, + Name = name, + ToolCalls=toolCalls, + }; + } + + /// + /// 创建工具消息 + /// + /// 系统消息内容 + /// 工具调用 ID,此消息正在响应的工具调用。 + /// + public static ThorChatMessage CreateToolMessage(string content, string toolCallId = null) + { + return new() + { + Role = ThorChatMessageRoleConst.Tool, + Content = content, + ToolCallId= toolCallId + }; + } +} \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ThorChatMessageAudioContent.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ThorChatMessageAudioContent.cs new file mode 100644 index 00000000..8dd1fc85 --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ThorChatMessageAudioContent.cs @@ -0,0 +1,11 @@ + +using System.Text.Json.Serialization; + +public sealed class ThorChatMessageAudioContent +{ + [JsonPropertyName("data")] + public string? Data { get; set; } + + [JsonPropertyName("format")] + public string? Format { get; set; } +} \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ThorChatMessageContent.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ThorChatMessageContent.cs new file mode 100644 index 00000000..e5083f1a --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ThorChatMessageContent.cs @@ -0,0 +1,98 @@ +using System.Text.Json.Serialization; + +namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi; + +/// +/// 发出的消息内容,包含图文,一般是一文一图,一文多图两种情况,请使用CreeateXXX系列方法构建内容 +/// +public class ThorChatMessageContent +{ + public ThorChatMessageContent() + { + + } + + /// + /// 消息内容类型,只能使用 定义的值赋值,如:ThorMessageContentTypeConst.Text + /// + [JsonPropertyName("type")] + public string Type { get; set; } + + /// + /// 消息内容类型为 text 时候的赋值,如:图片上描述了什么 + /// + [JsonPropertyName("text")] + public string? Text { get; set; } + + /// + /// 消息内容类型为 image_url 时候的赋值 + /// + [JsonPropertyName("image_url")] + public ThorVisionImageUrl? ImageUrl { get; set; } + + /// + /// 音频消息内容,包含音频数据和格式信息。 + /// + [JsonPropertyName("input_audio")] + public ThorChatMessageAudioContent? InputAudio { get; set; } + + /// + /// 创建文本类消息 + /// 文本内容 + /// + public static ThorChatMessageContent CreateTextContent(string text) + { + return new() + { + Type = ThorMessageContentTypeConst.Text, + Text = text + }; + } + + /// + /// 创建图片类消息,图片url形式 + /// 图片 url + /// 指定图像的详细程度。通过控制 detail 参数(该参数具有三个选项: low 、 high 或 auto ),您 + /// 可以控制模型的处理方式图像并生成其文本理解。默认情况下,模型将使用 auto 设置, + /// 该设置将查看图像输入大小并决定是否应使用 low 或 high 设置。 + /// + public static ThorChatMessageContent CreateImageUrlContent(string imageUrl, string? detail = "auto") + { + return new() + { + Type = ThorMessageContentTypeConst.ImageUrl, + ImageUrl = new() + { + Url = imageUrl, + Detail = detail + } + }; + } + + /// + /// 创建图片类消息,字节流转base64字符串形式 + /// The image binary data as byte array + /// 图片类型,如 png,jpg + /// 指定图像的详细程度。 + /// + public static ThorChatMessageContent CreateImageBinaryContent( + byte[] binaryImage, + string imageType, + string? detail = "auto" + ) + { + return new() + { + Type = ThorMessageContentTypeConst.ImageUrl, + ImageUrl = new() + { + Url = string.Format( + "data:image/{0};base64,{1}", + imageType, + Convert.ToBase64String(binaryImage) + ), + Detail = detail + } + }; + } +} diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ThorChatMessageFunction.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ThorChatMessageFunction.cs new file mode 100644 index 00000000..20c08142 --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ThorChatMessageFunction.cs @@ -0,0 +1,36 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi +{ + /// + /// 模型调用的函数。 + /// + public class ThorChatMessageFunction + { + /// + /// 功能名,如:get_current_weather + /// + [JsonPropertyName("name")] + public string? Name { get; set; } + + /// + /// 调用函数所用的参数,由模型以 JSON 格式生成。请注意,该模型并不总是生成有效的 JSON, + /// 并且可能会产生未由函数架构定义的参数。 + /// 在调用函数之前验证代码中的参数。 + /// 如:"{\"location\": \"San Francisco, USA\", \"format\": \"celsius\"}" + /// + [JsonPropertyName("arguments")] + public string? Arguments { get; set; } + + /// + /// 转换参数为字典 + /// + /// + public Dictionary ParseArguments() + { + var result = string.IsNullOrWhiteSpace(Arguments) == false ? JsonSerializer.Deserialize>(Arguments) : new Dictionary(); + return result; + } + } +} diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ThorChatMessageRoleConst.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ThorChatMessageRoleConst.cs new file mode 100644 index 00000000..97a16645 --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ThorChatMessageRoleConst.cs @@ -0,0 +1,45 @@ +namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi +{ + /// + /// 对话消息角色定义 + /// + public class ThorChatMessageRoleConst + { + /// + /// 系统角色 + /// + /// 用于为聊天助手分配特定的行为或上下文,以影响对话的模型行为。 + /// 例如,可以将系统角色设定为“您是足球专家”, + /// 那么 ChatGPT 在对话中会表现出特定的个性或专业知识。 + /// + /// + public static string System => "system"; + + /// + /// 用户角色 + /// + /// 代表实际的最终用户,向 ChatGPT 发送提示或消息, + /// 用于指示消息/提示来自最终用户或人类。 + /// + /// + public static string User => "user"; + + /// + /// 助手角色 + /// + /// 表示对最终用户提示的响应实体,用于保持对话的连贯性。 + /// 它是由模型自动生成并回复的,用于设置模型的先前响应,以继续对话流程。 + /// + /// + public static string Assistant => "assistant"; + + /// + /// 工具角色 + /// + /// 表示对最终用户提示的响应实体,用于保持对话的连贯性。 + /// 它是由模型自动生成并回复的,用于设置模型的先前响应,以继续对话流程。 + /// + /// + public static string Tool => "tool"; + } +} diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ThorError.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ThorError.cs new file mode 100644 index 00000000..baf1d5dc --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ThorError.cs @@ -0,0 +1,70 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi +{ + public class ThorError + { + /// + /// 错误码 + /// + [JsonPropertyName("code")] + public string? Code { get; set; } + + /// + /// 参数 + /// + [JsonPropertyName("param")] + public string? Param { get; set; } + + /// + /// 类型 + /// + [JsonPropertyName("type")] + public string? Type { get; set; } + + /// + /// 错误信息 + /// + [JsonIgnore] + public string? Message { get; private set; } + + /// + /// 错误信息 + /// + [JsonIgnore] + public List Messages { get; private set; } + + /// + /// 错误信息 + /// + [JsonPropertyName("message")] + public object MessageObject + { + set + { + switch (value) + { + case string s: + Message = s; + Messages = new() { s }; + break; + case List list when list.All(i => i is JsonElement): + Messages = list.Cast().Select(e => e.GetString()).ToList(); + Message = string.Join(Environment.NewLine, Messages); + break; + } + } + + get + { + if (Messages?.Count > 1) + { + return Messages; + } + + return Message; + } + } + } +} diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ThorMessageContentTypeConst.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ThorMessageContentTypeConst.cs new file mode 100644 index 00000000..4c62723e --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ThorMessageContentTypeConst.cs @@ -0,0 +1,23 @@ +namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi +{ + /// + /// 支持图片识别的消息体内容类型 + /// + public class ThorMessageContentTypeConst + { + /// + /// 文本内容 + /// + public static string Text => "text"; + + /// + /// 图片 Url 类型 + /// + public static string ImageUrl => "image_url"; + + /// + /// 图片 Url 类型 + /// + public static string Image => "image"; + } +} diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ThorResponseFormat.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ThorResponseFormat.cs new file mode 100644 index 00000000..d5f9d128 --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ThorResponseFormat.cs @@ -0,0 +1,21 @@ +using System.Text.Json.Serialization; + +namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi; + +/// +/// 指定模型必须输出的格式的对象。用于启用JSON模式。 +/// +public class ThorResponseFormat +{ + /// + /// 设置为json_object启用json模式。 + /// 这保证了模型生成的消息是有效的JSON。 + /// 注意,如果finish_reason=“length”,则消息内容可能是部分的, + /// 这表示生成超过了max_tokens或对话超过了最大上下文长度。 + /// + [JsonPropertyName("type")] + public string? Type { get; set; } + + [JsonPropertyName("json_schema")] + public ThorResponseJsonSchema JsonSchema { get; set; } +} \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ThorResponseJsonSchema.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ThorResponseJsonSchema.cs new file mode 100644 index 00000000..b89a65f1 --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ThorResponseJsonSchema.cs @@ -0,0 +1,18 @@ +using System.Text.Json.Serialization; + +namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi; + +public class ThorResponseJsonSchema +{ + [JsonPropertyName("name")] + public string Name { get; set; } + + [JsonPropertyName("description")] + public string? Description { get; set; } + + [JsonPropertyName("strict")] + public bool? Strict { get; set; } + + [JsonPropertyName("schema")] + public object Schema { get; set; } +} \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ThorStreamOptions.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ThorStreamOptions.cs new file mode 100644 index 00000000..bb3a639c --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ThorStreamOptions.cs @@ -0,0 +1,18 @@ +using System.Text.Json.Serialization; + +namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi +{ + /// + /// 流响应选项。仅当您设置 stream: true 时才设置此项。 + /// + public class ThorStreamOptions + { + /// + /// 如果设置,则会在 data: [DONE] 消息之前传输附加块。 + /// 该块上的 usage 字段显示整个请求的令牌使用统计信息, + /// choices 字段将始终为空数组。所有其他块也将包含一个 usage 字段,但具有空值。 + /// + [JsonPropertyName("include_usage")] + public bool? IncludeUsage { get; set; } + } +} diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ThorToolCall.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ThorToolCall.cs new file mode 100644 index 00000000..b58baab8 --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ThorToolCall.cs @@ -0,0 +1,38 @@ +using System.Text.Json.Serialization; + +namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi; + +/// +/// 工具调用对象定义 +/// +public class ThorToolCall +{ + public ThorToolCall() + { + Id = Guid.NewGuid().ToString("N"); + } + + /// + /// 工具调用序号值 + /// + [JsonPropertyName("index")] + public int Index { get; set; } + + /// + /// 工具调用的 ID + /// + [JsonPropertyName("id")] + public string? Id { get; set; } + + /// + /// 工具的类型。目前仅支持 function + /// + [JsonPropertyName("type")] + public string? Type { get; set; } = "function"; + + /// + /// 模型调用的函数。 + /// + [JsonPropertyName("function")] + public ThorChatMessageFunction? Function { get; set; } +} \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ThorToolChoice.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ThorToolChoice.cs new file mode 100644 index 00000000..47bfd6ab --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ThorToolChoice.cs @@ -0,0 +1,55 @@ +using System.Text.Json.Serialization; + +namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi; + +/// +/// 工具 +/// +public class ThorToolChoice +{ + /// + /// 表示模型不会调用任何工具 + /// + public static ThorToolChoice GetNone() => new() { Type = ThorToolChoiceTypeConst.None }; + + /// + /// 表示模型可以在生成消息或调用一个或多个工具之间进行选择 + /// + public static ThorToolChoice GetAuto() => new() { Type = ThorToolChoiceTypeConst.Auto }; + + /// + /// 表示模型必须调用一个或多个工具 + /// + public static ThorToolChoice GetRequired() => new() { Type = ThorToolChoiceTypeConst.Required }; + + /// + /// 指定特定工具会强制模型调用该工具 + /// + /// 函数名 + /// + public static ThorToolChoice GetFunction(string functionName) => new() + { + Type = ThorToolChoiceTypeConst.Function, + Function = new ThorToolChoiceFunctionTool() + { + Name = functionName + } + }; + + /// + /// "none" 表示模型不会调用任何工具
+ /// "auto" 表示模型可以在生成消息或调用一个或多个工具之间进行选择
+ /// "required" 表示模型必须调用一个或多个工具
+ /// "function" 指定特定工具会强制模型调用该工具
+ /// 使用 赋值 + ///
+ [JsonPropertyName("type")] + public string Type { get; set; } + + /// + /// 调用的函数定义 + /// + [JsonPropertyName("function")] + public ThorToolChoiceFunctionTool? Function { get; set; } + +} \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ThorToolChoiceFunctionTool.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ThorToolChoiceFunctionTool.cs new file mode 100644 index 00000000..45cfa85a --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ThorToolChoiceFunctionTool.cs @@ -0,0 +1,10 @@ +using System.Text.Json.Serialization; + +namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi +{ + public class ThorToolChoiceFunctionTool + { + [JsonPropertyName("name")] + public string Name { get; set; } + } +} diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ThorToolChoiceTypeConst.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ThorToolChoiceTypeConst.cs new file mode 100644 index 00000000..38811204 --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ThorToolChoiceTypeConst.cs @@ -0,0 +1,25 @@ +namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi +{ + public class ThorToolChoiceTypeConst + { + /// + /// 指定特定工具会强制模型调用该工具 + /// + public static string Function => "function"; + + /// + /// 表示模型可以在生成消息或调用一个或多个工具之间进行选择 + /// + public static string Auto => "auto"; + + /// + /// 表示模型不会调用任何工具 + /// + public static string None => "none"; + + /// + /// 表示模型必须调用一个或多个工具 + /// + public static string Required => "required "; + } +} diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ThorToolDefinition.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ThorToolDefinition.cs new file mode 100644 index 00000000..cc86cab5 --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ThorToolDefinition.cs @@ -0,0 +1,32 @@ +using System.Text.Json.Serialization; + +namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi; + +/// +/// 有效工具的定义。 +/// +public class ThorToolDefinition +{ + /// + /// 必修的。工具的类型。目前仅支持 function 。 + /// + [JsonPropertyName("type")] + public string Type { get; set; } = ThorToolTypeConst.Function; + + /// + /// 函数对象 + /// + [JsonPropertyName("function")] + public ThorToolFunctionDefinition? Function { get; set; } + + /// + /// 创建函数工具 + /// + /// + /// + public static ThorToolDefinition CreateFunctionTool(ThorToolFunctionDefinition function) => new() + { + Type = ThorToolTypeConst.Function, + Function = function + }; +} \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ThorToolFunctionDefinition.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ThorToolFunctionDefinition.cs new file mode 100644 index 00000000..f78d01ca --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ThorToolFunctionDefinition.cs @@ -0,0 +1,34 @@ +using System.Text.Json.Serialization; + +namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi; + +/// +/// 有效函数调用的定义。 +/// +public class ThorToolFunctionDefinition +{ + [JsonPropertyName("type")] + public string? Type { get; set; } + + /// + /// 要调用的函数的名称。必须是 a-z、A-Z、0-9 或包含下划线和破折号,最大长度为 64。 + /// + [JsonPropertyName("name")] + public string Name { get; set; } + + /// + /// 函数功能的描述,模型使用它来选择何时以及如何调用函数。 + /// + [JsonPropertyName("description")] + public string? Description { get; set; } + + /// + /// 函数接受的参数,描述为 JSON 架构对象。有关示例,请参阅指南,有关格式的文档,请参阅 JSON 架构参考。 + /// 省略 parameters 定义一个参数列表为空的函数。 + /// See the guide for examples, + /// and the JSON Schema reference for + /// documentation about the format. + /// + [JsonPropertyName("parameters")] + public ThorToolFunctionPropertyDefinition Parameters { get; set; } +} \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ThorToolFunctionPropertyDefinition.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ThorToolFunctionPropertyDefinition.cs new file mode 100644 index 00000000..380117c2 --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ThorToolFunctionPropertyDefinition.cs @@ -0,0 +1,260 @@ +using System.Text.Json.Serialization; + +namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi; + +/// +/// 函数参数是JSON格式对象 +/// https://json-schema.org/understanding-json-schema/reference/object.html +/// +/// +/// 定义属性示例: +/// [JsonPropertyName("location")] +/// public ThorToolFunctionPropertyDefinition Location = ThorToolFunctionPropertyDefinition.DefineString("The city and state, e.g. San Francisco, CA"); +/// +/// [JsonPropertyName("unit")] +/// public ThorToolFunctionPropertyDefinition Unit = ThorToolFunctionPropertyDefinition.DefineEnum(["celsius", "fahrenheit"]); +/// +public class ThorToolFunctionPropertyDefinition +{ + /// + /// 定义了函数对象的类型枚举 + /// + public enum FunctionObjectTypes + { + /// + /// 表示字符串类型的函数对象 + /// + String, + /// + /// 表示整数类型的函数对象 + /// + Integer, + /// + /// 表示数字(包括浮点数等)类型的函数对象 + /// + Number, + /// + /// 表示对象类型的函数对象 + /// + Object, + /// + /// 表示数组类型的函数对象 + /// + Array, + /// + /// 表示布尔类型的函数对象 + /// + Boolean, + /// + /// 表示空值类型的函数对象 + /// + Null + } + + /// + /// 必填的。函数参数对象类型。默认值为“object”。 + /// + [JsonPropertyName("type")] + public string Type { get; set; } = "object"; + + /// + /// 可选。“函数参数”列表,作为从参数名称映射的字典 + /// 对于描述类型的对象,可能还有可能的枚举值等等。 + /// + [JsonPropertyName("properties")] + public IDictionary? Properties { get; set; } + + /// + /// 可选。列出必需的“function arguments”列表。 + /// + [JsonPropertyName("required")] + public List? Required { get; set; } + + /// + /// 可选。是否允许附加属性。默认值为true。 + /// + [JsonPropertyName("additionalProperties")] + public bool? AdditionalProperties { get; set; } + + /// + /// 可选。参数描述。 + /// + [JsonPropertyName("description")] + public string? Description { get; set; } + + /// + /// 可选。此参数的允许值列表。 + /// + [JsonPropertyName("enum")] + public List? Enum { get; set; } + + /// + /// 可以使用minProperties和maxProperties关键字限制对象上的属性数量。每一个 + /// 这些必须是非负整数。 + /// + [JsonPropertyName("minProperties")] + public int? MinProperties { get; set; } + + /// + /// 可以使用minProperties和maxProperties关键字限制对象上的属性数量。每一个 + /// 这些必须是非负整数。 + /// + [JsonPropertyName("maxProperties")] + public int? MaxProperties { get; set; } + + /// + /// 如果type为“array”,则指定数组中所有项目的元素类型。 + /// 如果类型不是“array”,则应为null。 + /// 有关更多详细信息,请参阅 https://json-schema.org/understanding-json-schema/reference/array.html + /// + [JsonPropertyName("items")] + public ThorToolFunctionPropertyDefinition? Items { get; set; } + + /// + /// 定义数组 + /// + /// + /// + public static ThorToolFunctionPropertyDefinition DefineArray(ThorToolFunctionPropertyDefinition? arrayItems = null) + { + return new ThorToolFunctionPropertyDefinition + { + Items = arrayItems, + Type = ConvertTypeToString(FunctionObjectTypes.Array) + }; + } + + /// + /// 定义枚举 + /// + /// + /// + /// + public static ThorToolFunctionPropertyDefinition DefineEnum(List enumList, string? description = null) + { + return new ThorToolFunctionPropertyDefinition + { + Description = description, + Enum = enumList, + Type = ConvertTypeToString(FunctionObjectTypes.String) + }; + } + + /// + /// 定义整型 + /// + /// + /// + public static ThorToolFunctionPropertyDefinition DefineInteger(string? description = null) + { + return new ThorToolFunctionPropertyDefinition + { + Description = description, + Type = ConvertTypeToString(FunctionObjectTypes.Integer) + }; + } + + /// + /// 定义数字 + /// + /// + /// + public static ThorToolFunctionPropertyDefinition DefineNumber(string? description = null) + { + return new ThorToolFunctionPropertyDefinition + { + Description = description, + Type = ConvertTypeToString(FunctionObjectTypes.Number) + }; + } + + /// + /// 定义字符串 + /// + /// + /// + public static ThorToolFunctionPropertyDefinition DefineString(string? description = null) + { + return new ThorToolFunctionPropertyDefinition + { + Description = description, + Type = ConvertTypeToString(FunctionObjectTypes.String) + }; + } + + /// + /// 定义布尔值 + /// + /// + /// + public static ThorToolFunctionPropertyDefinition DefineBoolean(string? description = null) + { + return new ThorToolFunctionPropertyDefinition + { + Description = description, + Type = ConvertTypeToString(FunctionObjectTypes.Boolean) + }; + } + + /// + /// 定义null + /// + /// + /// + public static ThorToolFunctionPropertyDefinition DefineNull(string? description = null) + { + return new ThorToolFunctionPropertyDefinition + { + Description = description, + Type = ConvertTypeToString(FunctionObjectTypes.Null) + }; + } + + /// + /// 定义对象 + /// + /// + /// + /// + /// + /// + /// + public static ThorToolFunctionPropertyDefinition DefineObject(IDictionary? properties, + List? required, + bool? additionalProperties, + string? description, + List? @enum) + { + return new ThorToolFunctionPropertyDefinition + { + Properties = properties, + Required = required, + AdditionalProperties = additionalProperties, + Description = description, + Enum = @enum, + Type = ConvertTypeToString(FunctionObjectTypes.Object) + }; + } + + + /// + /// 将 `FunctionObjectTypes` 枚举值转换为其对应的字符串表示形式。 + /// + /// 要转换的类型 + /// 给定类型的字符串表示形式 + + public static string ConvertTypeToString(FunctionObjectTypes type) + { + return type switch + { + FunctionObjectTypes.String => "string", + FunctionObjectTypes.Integer => "integer", + FunctionObjectTypes.Number => "number", + FunctionObjectTypes.Object => "object", + FunctionObjectTypes.Array => "array", + FunctionObjectTypes.Boolean => "boolean", + FunctionObjectTypes.Null => "null", + _ => throw new ArgumentOutOfRangeException(nameof(type), $"Unknown type: {type}") + }; + } +} \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ThorToolTypeConst.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ThorToolTypeConst.cs new file mode 100644 index 00000000..cd2b18a0 --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ThorToolTypeConst.cs @@ -0,0 +1,13 @@ +namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi +{ + /// + /// 工具类型定义 + /// + public class ThorToolTypeConst + { + /// + /// 函数 + /// + public static string Function => "function"; + } +} diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ThorUsageResponse.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ThorUsageResponse.cs new file mode 100644 index 00000000..a3214ac0 --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ThorUsageResponse.cs @@ -0,0 +1,102 @@ +using System.Text.Json.Serialization; + +namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi; + +/// +/// 统计信息模型 +/// +public record ThorUsageResponse +{ + /// + /// 提示中的令牌数。 + /// + [JsonPropertyName("prompt_tokens")] + public int? PromptTokens { get; set; } + + [JsonPropertyName("input_tokens")] + public int? InputTokens { get; set; } + + [JsonPropertyName("output_tokens")] + public int? OutputTokens { get; set; } + + [JsonPropertyName("input_tokens_details")] + public ThorUsageResponseInputTokensDetails? InputTokensDetails { get; set; } + + /// + /// 生成的完成中的令牌数。 + /// + [JsonPropertyName("completion_tokens")] + public long? CompletionTokens { get; set; } + + /// + /// 请求中使用的令牌总数(提示 + 完成)。 + /// + [JsonPropertyName("total_tokens")] + public long? TotalTokens { get; set; } + + /// + /// ThorUsageResponsePromptTokensDetails + /// + [JsonPropertyName("prompt_tokens_details")] + public ThorUsageResponsePromptTokensDetails? PromptTokensDetails { get; set; } + + /// + /// ThorUsageResponseCompletionTokensDetails + /// + [JsonPropertyName("completion_tokens_details")] + public ThorUsageResponseCompletionTokensDetails? CompletionTokensDetails { get; set; } +} + +public class ThorUsageResponseInputTokensDetails +{ + [JsonPropertyName("image_tokens")] + public int? ImageTokens { get; set; } + + [JsonPropertyName("text_tokens")] + public int? TextTokens { get; set; } +} + +public record ThorUsageResponsePromptTokensDetails +{ + /// + /// 缓存的令牌数。 + /// + [JsonPropertyName("cached_tokens")] + public int? CachedTokens { get; set; } + + /// + /// audio_tokens + /// + [JsonPropertyName("audio_tokens")] + public int? AudioTokens { get; set; } +} + +/// +/// completion_tokens_details +/// +public record ThorUsageResponseCompletionTokensDetails +{ + /// + /// 使用 Predicted Outputs 时, Prediction 的 Final。 + /// + [JsonPropertyName("accepted_prediction_tokens")] + public int? AcceptedPredictionTokens { get; set; } + + /// + /// 模型生成的音频输入令牌。 + /// + [JsonPropertyName("audio_tokens")] + public int? AudioTokens { get; set; } + + /// + /// 模型生成的用于推理的 Token。 + /// + [JsonPropertyName("reasoning_tokens")] + public int? ReasoningTokens { get; set; } + + /// + /// 使用 Predicted Outputs 时, 预测,但未出现在 completion 中。但是,与 reasoning 令牌,这些令牌仍然计入总数 用于 Billing、Output 和 Context Window 的完成令牌 限制。 + /// + [JsonPropertyName("rejected_prediction_tokens")] + public int? RejectedPredictionTokens { get; set; } +} \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ThorVisionImageUrl.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ThorVisionImageUrl.cs new file mode 100644 index 00000000..f2917457 --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ThorVisionImageUrl.cs @@ -0,0 +1,29 @@ +using System.Text.Json.Serialization; + +namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi; + +/// +/// 图片消息内容对象 +/// +public class ThorVisionImageUrl +{ + /// + /// 图片的url地址,如:https://localhost/logo.jpg ,一般只支持 .png , .jpg .webp .gif + /// 也可以是base64字符串,如:data:image/jpeg;base64,{base64_image} + /// 要看底层平台具体要求 + /// + [JsonPropertyName("url")] + public string Url { get; set; } + + /// + /// 指定图像的细节级别。在愿景指南中了解更多信息。https://platform.openai.com/docs/guides/vision/low-or-high-fidelity-image-understanding + /// + /// 指定图像的详细程度。通过控制 detail 参数(该参数具有三个选项: low 、 high 或 auto ),您 + /// 可以控制模型的处理方式图像并生成其文本理解。默认情况下,模型将使用 auto 设置, + /// 该设置将查看图像输入大小并决定是否应使用 low 或 high 设置。 + /// + /// + [JsonPropertyName("detail")] + public string? Detail { get; set; } = "auto"; + +} \ No newline at end of file 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 5508d6de..503565ee 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 @@ -12,6 +12,7 @@ using OpenAI.Chat; using Volo.Abp.Application.Services; using Volo.Abp.Users; using Yi.Framework.AiHub.Application.Contracts.Dtos; +using Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi; using Yi.Framework.AiHub.Domain.Entities; using Yi.Framework.AiHub.Domain.Entities.Model; using Yi.Framework.AiHub.Domain.Extensions; @@ -92,7 +93,8 @@ public class AiChatService : ApplicationService /// /// /// - public async Task PostSendAsync(SendMessageInput input, CancellationToken cancellationToken) + public async Task PostSendAsync([FromBody] ThorChatCompletionsRequest input, [FromQuery] Guid sessionId, + CancellationToken cancellationToken) { //除了免费模型,其他的模型都要校验 if (!input.Model.Contains("DeepSeek-R1")) @@ -111,22 +113,8 @@ public class AiChatService : ApplicationService throw new UserFriendlyException("未登录用户,只能使用未加速的DeepSeek-R1,请登录后重试"); } } - - var history = new List(); - foreach (var aiChatContextDto in input.Messages) - { - if (aiChatContextDto.Role == "assistant") - { - history.Add(ChatMessage.CreateAssistantMessage(aiChatContextDto.Content)); - } - else if (aiChatContextDto.Role == "user") - { - history.Add(ChatMessage.CreateUserMessage(aiChatContextDto.Content)); - } - } - //ai网关代理httpcontext - await _aiGateWayManager.CompleteChatStreamForStatisticsAsync(_httpContextAccessor.HttpContext, input.Model, history, - CurrentUser.Id, input.SessionId, cancellationToken); + await _aiGateWayManager.CompleteChatStreamForStatisticsAsync(_httpContextAccessor.HttpContext, input, + CurrentUser.Id, sessionId, cancellationToken); } } \ No newline at end of file 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 ef9b0357..4df08fe8 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 @@ -1,12 +1,10 @@ -using Dm.util; -using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; -using OpenAI.Chat; -using SqlSugar; using Volo.Abp.Application.Services; -using Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAiDto; +using Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi; using Yi.Framework.AiHub.Domain.Entities.Model; +using Yi.Framework.AiHub.Domain.Extensions; using Yi.Framework.AiHub.Domain.Managers; using Yi.Framework.SqlSugarCore.Abstractions; @@ -37,45 +35,22 @@ public class OpenApiService : ApplicationService /// /// [HttpPost("openApi/v1/chat/completions")] - public async Task ChatCompletionsAsync([FromBody] ChatCompletionsInput input, CancellationToken cancellationToken) + public async Task ChatCompletionsAsync([FromBody] ThorChatCompletionsRequest input, + CancellationToken cancellationToken) { //前面都是校验,后面才是真正的调用 var httpContext = this._httpContextAccessor.HttpContext; - var userId = await _tokenManager.GetUserIdAsync(GetTokenByHttpContext(httpContext)); - var history = new List(); - if (!string.IsNullOrWhiteSpace(input.Prompt)) - { - history.add(ChatMessage.CreateSystemMessage(input.Prompt)); - } - foreach (var aiChatContextDto in input.Messages) - { - if (aiChatContextDto.Role == "assistant") - { - history.Add(ChatMessage.CreateAssistantMessage(aiChatContextDto.ConvertContent())); - } - else if (aiChatContextDto.Role == "user") - { - history.Add(ChatMessage.CreateUserMessage(aiChatContextDto.ConvertContent())); - } - else if (aiChatContextDto.Role == "system") - { - history.Add(ChatMessage.CreateSystemMessage(aiChatContextDto.ConvertContent())); - } - } - - //是否使用流式传输 + //ai网关代理httpcontext if (input.Stream == true) { - //ai网关代理httpcontext - await _aiGateWayManager.CompleteChatStreamForStatisticsAsync(_httpContextAccessor.HttpContext, input.Model, - history, + await _aiGateWayManager.CompleteChatStreamForStatisticsAsync(_httpContextAccessor.HttpContext, input, userId, null, cancellationToken); } else { - await _aiGateWayManager.CompleteChatForStatisticsAsync(_httpContextAccessor.HttpContext, input.Model, - history, userId, null, + await _aiGateWayManager.CompleteChatForStatisticsAsync(_httpContextAccessor.HttpContext, input, userId, + null, cancellationToken); } } @@ -85,19 +60,20 @@ public class OpenApiService : ApplicationService /// /// [HttpGet("openApi/v1/models")] - public async Task ModelsAsync() + public async Task ModelsAsync() { var data = await _aiModelRepository._DbQueryable .OrderByDescending(x => x.OrderNum) - .Select(x => new ModelDataOutput + .Select(x => new ModelsDataDto { - ModelId = x.ModelId, - Object = "model", - Owned_by = "organization-owner", - Permission = new List() + Id = x.ModelId, + @object = "model", + Created = DateTime.Now.ToUnixTimeSeconds(), + OwnedBy = "organization-owner", + Type = x.ModelId }).ToListAsync(); - return new ModelGetOutput() + return new ModelsListDto() { Data = data }; diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Dtos/AiModelDescribe.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Dtos/AiModelDescribe.cs index 06572d65..ca226e95 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Dtos/AiModelDescribe.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Dtos/AiModelDescribe.cs @@ -46,4 +46,10 @@ public class AiModelDescribe /// 模型描述 /// public string? Description { get; set; } + + + /// + /// 额外url + /// + public string? ExtraUrl { get; set; } } \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Dtos/CompleteChatResponse.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Dtos/CompleteChatResponse.cs deleted file mode 100644 index b0ceb1cd..00000000 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Dtos/CompleteChatResponse.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Yi.Framework.AiHub.Domain.Shared.Dtos; - -public class CompleteChatResponse -{ - public TokenUsage? TokenUsage { get; set; } - public bool IsFinish { get; set; } - public string? Content { get; set; } -} \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiChat/ChatErrorException.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiChat/ChatErrorException.cs deleted file mode 100644 index 97cf38fa..00000000 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiChat/ChatErrorException.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Microsoft.Extensions.Logging; - -namespace Yi.Framework.AiHub.Domain.AiChat; - -public class ChatErrorException : Exception -{ - public string? Code { get; set; } - - public string? Details { get; set; } - - public LogLevel LogLevel { get; set; } - - public ChatErrorException( - string? code = null, - string? message = null, - string? details = null, - Exception? innerException = null, - LogLevel logLevel = LogLevel.Warning) - : base(message, innerException) - { - this.Code = code; - this.Details = details; - this.LogLevel = logLevel; - } - - public ChatErrorException WithData(string name, object value) - { - this.Data[(object)name] = value; - return this; - } -} \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiChat/Impl/AzureChatService.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiChat/Impl/AzureChatService.cs deleted file mode 100644 index a4853c68..00000000 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiChat/Impl/AzureChatService.cs +++ /dev/null @@ -1,102 +0,0 @@ -using System.Runtime.CompilerServices; -using Azure; -using Azure.AI.OpenAI; -using OpenAI.Chat; -using Yi.Framework.AiHub.Domain.Shared.Dtos; - -namespace Yi.Framework.AiHub.Domain.AiChat.Impl; - -public class AzureChatService : IChatService -{ - public AzureChatService() - { - } - - public async IAsyncEnumerable CompleteChatStreamAsync(AiModelDescribe aiModelDescribe, - List messages, - [EnumeratorCancellation] CancellationToken cancellationToken) - { - var endpoint = new Uri(aiModelDescribe.Endpoint); - - var deploymentName = aiModelDescribe.ModelId; - var apiKey = aiModelDescribe.ApiKey; - - AzureOpenAIClient azureClient = new( - endpoint, - new AzureKeyCredential(apiKey), new AzureOpenAIClientOptions() - { - NetworkTimeout = TimeSpan.FromSeconds(600), - }); - - ChatClient chatClient = azureClient.GetChatClient(deploymentName); - - var response = chatClient.CompleteChatStreamingAsync(messages, new ChatCompletionOptions() - { - // MaxOutputTokenCount = 2048 - }, cancellationToken: cancellationToken); - - await foreach (StreamingChatCompletionUpdate update in response) - { - var result = new CompleteChatResponse(); - var isFinish = update.Usage?.OutputTokenCount is not null; - if (isFinish) - { - result.IsFinish = true; - result.TokenUsage = new TokenUsage - { - OutputTokenCount = update.Usage.OutputTokenCount, - InputTokenCount = update.Usage.InputTokenCount, - TotalTokenCount = update.Usage.TotalTokenCount - }; - } - - foreach (ChatMessageContentPart updatePart in update.ContentUpdate) - { - result.Content = updatePart.Text; - yield return result; - } - - if (isFinish) - { - yield return result; - } - } - } - - public async Task CompleteChatAsync(AiModelDescribe aiModelDescribe, - List messages, CancellationToken cancellationToken) - { - var endpoint = new Uri(aiModelDescribe.Endpoint); - - var deploymentName = aiModelDescribe.ModelId; - var apiKey = aiModelDescribe.ApiKey; - - AzureOpenAIClient azureClient = new( - endpoint, - new AzureKeyCredential(apiKey), new AzureOpenAIClientOptions() - { - NetworkTimeout = TimeSpan.FromSeconds(600), - }); - - ChatClient chatClient = azureClient.GetChatClient(deploymentName); - - var response = await chatClient.CompleteChatAsync(messages, new ChatCompletionOptions() - { - // MaxOutputTokenCount = 2048 - }, cancellationToken: cancellationToken); - - var output = new CompleteChatResponse - { - TokenUsage = new TokenUsage() - { - OutputTokenCount = response?.Value.Usage?.OutputTokenCount ?? 0, - InputTokenCount = response?.Value.Usage?.InputTokenCount ?? 0, - TotalTokenCount = response?.Value.Usage?.TotalTokenCount ?? 0 - }, - IsFinish = true, - Content = response?.Value.Content.FirstOrDefault()?.Text - }; - - return output; - } -} \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiChat/Impl/AzureRestChatService.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiChat/Impl/AzureRestChatService.cs deleted file mode 100644 index 4eb274e2..00000000 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiChat/Impl/AzureRestChatService.cs +++ /dev/null @@ -1,275 +0,0 @@ -using System.Net.Http.Json; -using System.Runtime.CompilerServices; -using System.Text; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using OpenAI.Chat; -using Yi.Framework.AiHub.Domain.Extensions; -using Yi.Framework.AiHub.Domain.Shared.Dtos; - -namespace Yi.Framework.AiHub.Domain.AiChat.Impl; - -public class AzureRestChatService : IChatService -{ - public AzureRestChatService() - { - } - - public async IAsyncEnumerable CompleteChatStreamAsync(AiModelDescribe aiModelDescribe, - List messages, - [EnumeratorCancellation] CancellationToken cancellationToken) - { - // 设置API URL - var apiUrl = $"{aiModelDescribe.Endpoint}"; - - // 准备请求内容 - var requestBody = new - { - messages = messages.Select(x => new - { - role = x.GetRoleAsString(), - content = x.Content.FirstOrDefault()?.Text - }).ToList(), - stream = true, - // max_tokens = 2048, - // temperature = 0.8, - // top_p = 0.1, - // presence_penalty = 0, - // frequency_penalty = 0, - model = aiModelDescribe.ModelId - }; - - // 序列化请求内容为JSON - string jsonBody = JsonConvert.SerializeObject(requestBody); - - using var httpClient = new HttpClient() - { - //10分钟超时 - Timeout = TimeSpan.FromSeconds(600) - }; - // 设置请求头 - httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {aiModelDescribe.ApiKey}"); - // 其他头信息如Content-Type在StringContent中设置 - - // 构造 POST 请求 - var request = new HttpRequestMessage(HttpMethod.Post, apiUrl); - - // 设置请求内容(示例) - request.Content = new StringContent(jsonBody, Encoding.UTF8, "application/json"); - - // 发送POST请求 - HttpResponseMessage response = - await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken); - - if (!response.IsSuccessStatusCode) - { - throw new UserFriendlyException( - $"当前模型不可用:{aiModelDescribe.ModelId},状态码:{response.StatusCode},原因:{response.ReasonPhrase}"); - } - // 确认响应成功 - // response.EnsureSuccessStatusCode(); - - // 读取响应内容 - var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken); - // 从流中读取数据并输出到控制台 - using var streamReader = new StreamReader(responseStream); - while (await streamReader.ReadLineAsync(cancellationToken) is { } line) - { - var result = new CompleteChatResponse(); - try - { - var jsonObj = MapToStreamJObject(line); - if (jsonObj is not null) - { - var content = GetStreamContent(jsonObj); - var tokenUsage = GetStreamTokenUsage(jsonObj); - result = new CompleteChatResponse - { - TokenUsage = tokenUsage, - IsFinish = tokenUsage is not null, - Content = content - }; - } - } - catch (Exception e) - { - Console.WriteLine("解析失败"); - } - - yield return result; - } - } - - public async Task CompleteChatAsync(AiModelDescribe aiModelDescribe, - List messages, CancellationToken cancellationToken) - { - // 设置API URL - var apiUrl = $"{aiModelDescribe.Endpoint}"; - - // 准备请求内容 - var requestBody = new - { - messages = messages.Select(x => new - { - role = x.GetRoleAsString(), - content = x.Content.FirstOrDefault()?.Text - }).ToList(), - stream = false, - // max_tokens = 2048, - // temperature = 0.8, - // top_p = 0.1, - // presence_penalty = 0, - // frequency_penalty = 0, - model = aiModelDescribe.ModelId - }; - - // 序列化请求内容为JSON - string jsonBody = JsonConvert.SerializeObject(requestBody); - - using var httpClient = new HttpClient() - { - //10分钟超时 - Timeout = TimeSpan.FromSeconds(600) - }; - // 设置请求头 - httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {aiModelDescribe.ApiKey}"); - // 其他头信息如Content-Type在StringContent中设置 - - // 构造 POST 请求 - var request = new HttpRequestMessage(HttpMethod.Post, apiUrl); - - // 设置请求内容(示例) - request.Content = new StringContent(jsonBody, Encoding.UTF8, "application/json"); - - // 发送POST请求 - HttpResponseMessage response = - await httpClient.SendAsync(request, cancellationToken); - - if (!response.IsSuccessStatusCode) - { - throw new UserFriendlyException( - $"当前模型不可用:{aiModelDescribe.ModelId},状态码:{response.StatusCode},原因:{response.ReasonPhrase}"); - } - // 确认响应成功 - // response.EnsureSuccessStatusCode(); - - // 读取响应内容 - var responseStr = await response.Content.ReadAsStringAsync(cancellationToken); - var jObject = MapToJObject(responseStr); - - var content = GetContent(jObject); - var usage = GetTokenUsage(jObject); - var result = new CompleteChatResponse - { - TokenUsage = usage, - IsFinish = true, - Content = content - }; - - return result; - } - - private JObject? MapToJObject(string body) - { - if (string.IsNullOrWhiteSpace(body)) - return null; - return JObject.Parse(body); - } - - private string? GetContent(JObject? jsonObj) - { - var contentToken = jsonObj.SelectToken("choices[0].message.content"); - if (contentToken != null && contentToken.Type != JTokenType.Null) - { - return contentToken.ToString(); - } - - return null; - } - - private TokenUsage? GetTokenUsage(JObject? jsonObj) - { - var usage = jsonObj.SelectToken("usage"); - if (usage is not null && usage.Type != JTokenType.Null) - { - var result = new TokenUsage(); - var completionTokens = usage["completion_tokens"]; - if (completionTokens is not null && completionTokens.Type != JTokenType.Null) - { - result.OutputTokenCount = completionTokens.ToObject(); - } - - var promptTokens = usage["prompt_tokens"]; - if (promptTokens is not null && promptTokens.Type != JTokenType.Null) - { - result.InputTokenCount = promptTokens.ToObject(); - } - - var totalTokens = usage["total_tokens"]; - if (totalTokens is not null && totalTokens.Type != JTokenType.Null) - { - result.TotalTokenCount = totalTokens.ToObject(); - } - - return result; - } - - return null; - } - - - private JObject? MapToStreamJObject(string line) - { - if (line == "data: [DONE]" || string.IsNullOrWhiteSpace(line)) - { - return null; - } - - if (string.IsNullOrWhiteSpace(line)) - return null; - string prefix = "data: "; - line = line.Substring(prefix.Length); - return JObject.Parse(line); - } - - private string? GetStreamContent(JObject? jsonObj) - { - var contentToken = jsonObj.SelectToken("choices[0].delta.content"); - if (contentToken != null && contentToken.Type != JTokenType.Null) - { - return contentToken.ToString(); - } - - return null; - } - - private TokenUsage? GetStreamTokenUsage(JObject? jsonObj) - { - var usage = jsonObj.SelectToken("usage"); - if (usage is not null && usage.Type != JTokenType.Null) - { - var result = new TokenUsage(); - var completionTokens = usage["completion_tokens"]; - if (completionTokens is not null && completionTokens.Type != JTokenType.Null) - { - result.OutputTokenCount = completionTokens.ToObject(); - } - - var promptTokens = usage["prompt_tokens"]; - if (promptTokens is not null && promptTokens.Type != JTokenType.Null) - { - result.InputTokenCount = promptTokens.ToObject(); - } - - var totalTokens = usage["total_tokens"]; - if (totalTokens is not null && totalTokens.Type != JTokenType.Null) - { - result.TotalTokenCount = totalTokens.ToObject(); - } - - return result; - } - - return null; - } -} \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiGateWay/Exceptions/PaymentRequiredException.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiGateWay/Exceptions/PaymentRequiredException.cs new file mode 100644 index 00000000..76f03bec --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiGateWay/Exceptions/PaymentRequiredException.cs @@ -0,0 +1,5 @@ +namespace Yi.Framework.AiHub.Domain.AiGateWay.Exceptions; + +public sealed class PaymentRequiredException() : Exception() +{ +} \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiGateWay/Exceptions/ThorRateLimitException.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiGateWay/Exceptions/ThorRateLimitException.cs new file mode 100644 index 00000000..d6dc4e0a --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiGateWay/Exceptions/ThorRateLimitException.cs @@ -0,0 +1,12 @@ +namespace Yi.Framework.AiHub.Domain.AiGateWay.Exceptions; + +public class ThorRateLimitException : Exception +{ + public ThorRateLimitException() + { + } + + public ThorRateLimitException(string message) : base(message) + { + } +} \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiGateWay/HttpClientExtensions.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiGateWay/HttpClientExtensions.cs new file mode 100644 index 00000000..dbfee79f --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiGateWay/HttpClientExtensions.cs @@ -0,0 +1,274 @@ +using System.Net.Http.Json; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Yi.Framework.AiHub.Domain.AiGateWay; + +public static class HttpClientExtensions +{ + public static async Task HttpRequestRaw(this HttpClient httpClient, string url, + object? postData, + string token) + { + HttpRequestMessage req = new(HttpMethod.Post, url); + + if (postData != null) + { + if (postData is HttpContent data) + { + req.Content = data; + } + else + { + string jsonContent = JsonSerializer.Serialize(postData, ThorJsonSerializer.DefaultOptions); + var stringContent = new StringContent(jsonContent, Encoding.UTF8, "application/json"); + req.Content = stringContent; + } + } + + if (!string.IsNullOrEmpty(token)) + { + req.Headers.Add("Authorization", $"Bearer {token}"); + } + + var response = await httpClient.SendAsync(req, HttpCompletionOption.ResponseHeadersRead); + + return response; + } + + public static async Task HttpRequestRaw(this HttpClient httpClient, string url, + object? postData, + string token, string tokenKey) + { + HttpRequestMessage req = new(HttpMethod.Post, url); + + if (postData != null) + { + if (postData is HttpContent data) + { + req.Content = data; + } + else + { + string jsonContent = JsonSerializer.Serialize(postData, ThorJsonSerializer.DefaultOptions); + var stringContent = new StringContent(jsonContent, Encoding.UTF8, "application/json"); + req.Content = stringContent; + } + } + + if (!string.IsNullOrEmpty(token)) + { + req.Headers.Add(tokenKey, token); + } + + + var response = await httpClient.SendAsync(req, HttpCompletionOption.ResponseHeadersRead); + + return response; + } + + public static async Task HttpRequestRaw(this HttpClient httpClient, string url, + object? postData, + string token, Dictionary headers) + { + HttpRequestMessage req = new(HttpMethod.Post, url); + + if (postData != null) + { + if (postData is HttpContent data) + { + req.Content = data; + } + else + { + string jsonContent = JsonSerializer.Serialize(postData, ThorJsonSerializer.DefaultOptions); + var stringContent = new StringContent(jsonContent, Encoding.UTF8, "application/json"); + req.Content = stringContent; + } + } + + if (!string.IsNullOrEmpty(token)) + { + req.Headers.Add("Authorization", $"Bearer {token}"); + } + + foreach (var header in headers) + { + req.Headers.Add(header.Key, header.Value); + } + + + var response = await httpClient.SendAsync(req, HttpCompletionOption.ResponseHeadersRead); + + return response; + } + + public static async Task HttpRequestRaw(this HttpClient httpClient, HttpRequestMessage req, + object? postData) + { + if (postData != null) + { + if (postData is HttpContent data) + { + req.Content = data; + } + else + { + string jsonContent = JsonSerializer.Serialize(postData, ThorJsonSerializer.DefaultOptions); + var stringContent = new StringContent(jsonContent, Encoding.UTF8, "application/json"); + req.Content = stringContent; + } + } + + var response = await httpClient.SendAsync(req, HttpCompletionOption.ResponseHeadersRead); + + return response; + } + + public static async Task PostJsonAsync(this HttpClient httpClient, string url, + object? postData, + string token) + { + HttpRequestMessage req = new(HttpMethod.Post, url); + + if (postData != null) + { + if (postData is HttpContent data) + { + req.Content = data; + } + else + { + var stringContent = + new StringContent(JsonSerializer.Serialize(postData, ThorJsonSerializer.DefaultOptions), + Encoding.UTF8, "application/json"); + req.Content = stringContent; + } + } + + if (!string.IsNullOrEmpty(token)) + { + req.Headers.Add("Authorization", $"Bearer {token}"); + } + + return await httpClient.SendAsync(req); + } + + public static async Task PostJsonAsync(this HttpClient httpClient, string url, + object? postData, + string token, Dictionary headers) + { + HttpRequestMessage req = new(HttpMethod.Post, url); + + if (postData != null) + { + if (postData is HttpContent data) + { + req.Content = data; + } + else + { + string jsonContent = JsonSerializer.Serialize(postData, ThorJsonSerializer.DefaultOptions); + var stringContent = new StringContent(jsonContent, Encoding.UTF8, "application/json"); + req.Content = stringContent; + } + } + + if (!string.IsNullOrEmpty(token)) + { + req.Headers.Add("Authorization", $"Bearer {token}"); + } + + foreach (var header in headers) + { + req.Headers.Add(header.Key, header.Value); + } + + return await httpClient.SendAsync(req); + } + + public static Task PostJsonAsync(this HttpClient httpClient, string url, object? postData, + string token, string tokenKey) + { + HttpRequestMessage req = new(HttpMethod.Post, url); + + if (postData != null) + { + if (postData is HttpContent data) + { + req.Content = data; + } + else + { + string jsonContent = JsonSerializer.Serialize(postData, ThorJsonSerializer.DefaultOptions); + var stringContent = new StringContent(jsonContent, Encoding.UTF8, "application/json"); + req.Content = stringContent; + } + } + + if (!string.IsNullOrEmpty(token)) + { + req.Headers.Add(tokenKey, token); + } + + return httpClient.SendAsync(req); + } + + + public static async Task PostAndReadAsAsync(this HttpClient client, string uri, + object? requestModel, CancellationToken cancellationToken = default) where TResponse : ThorBaseResponse, new() + { + var response = await client.PostAsJsonAsync(uri, requestModel, new JsonSerializerOptions + { + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault + }, cancellationToken); + return await HandleResponseContent(response, cancellationToken); + } + + public static async Task PostFileAndReadAsAsync(this HttpClient client, string uri, + HttpContent content, CancellationToken cancellationToken = default) where TResponse : ThorBaseResponse, new() + { + var response = await client.PostAsync(uri, content, cancellationToken); + return await HandleResponseContent(response, cancellationToken); + } + + public static async Task PostFileAndReadAsStringAsync(this HttpClient client, string uri, + HttpContent content, CancellationToken cancellationToken = default) + { + var response = await client.PostAsync(uri, content, cancellationToken); + return await response.Content.ReadAsStringAsync(cancellationToken) ?? throw new InvalidOperationException(); + } + + public static async Task DeleteAndReadAsAsync(this HttpClient client, string uri, + CancellationToken cancellationToken = default) where TResponse : ThorBaseResponse, new() + { + var response = await client.DeleteAsync(uri, cancellationToken); + return await HandleResponseContent(response, cancellationToken); + } + + private static async Task HandleResponseContent(this HttpResponseMessage response, + CancellationToken cancellationToken) where TResponse : ThorBaseResponse, new() + { + TResponse result; + + if (!response.Content.Headers.ContentType?.MediaType?.Equals("application/json", + StringComparison.OrdinalIgnoreCase) ?? true) + { + result = new() + { + Error = new() + { + MessageObject = await response.Content.ReadAsStringAsync(cancellationToken) + } + }; + } + else + { + result = await response.Content.ReadFromJsonAsync(cancellationToken: cancellationToken) ?? + throw new InvalidOperationException(); + } + + return result; + } +} \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiGateWay/HttpClientFactory.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiGateWay/HttpClientFactory.cs new file mode 100644 index 00000000..7048c436 --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiGateWay/HttpClientFactory.cs @@ -0,0 +1,73 @@ +using System.Collections.Concurrent; + +namespace Yi.Framework.AiHub.Domain.AiGateWay; + +public static class HttpClientFactory +{ + /// + /// HttpClient池总数 + /// + /// + private static int _poolSize; + + private static int PoolSize + { + get + { + if (_poolSize == 0) + { + // 获取环境变量 + var poolSize = Environment.GetEnvironmentVariable("HttpClientPoolSize"); + if (!string.IsNullOrEmpty(poolSize) && int.TryParse(poolSize, out var size)) + { + _poolSize = size; + } + else + { + _poolSize = Environment.ProcessorCount; + } + + if (_poolSize < 1) + { + _poolSize = 2; + } + } + + return _poolSize; + } + } + + private static readonly ConcurrentDictionary>> HttpClientPool = new(); + + public static HttpClient GetHttpClient(string key) + { + return HttpClientPool.GetOrAdd(key, k => new Lazy>(() => + { + var clients = new List(PoolSize); + + for (var i = 0; i < PoolSize; i++) + { + clients.Add(new HttpClient(new SocketsHttpHandler + { + PooledConnectionLifetime = TimeSpan.FromMinutes(30), + PooledConnectionIdleTimeout = TimeSpan.FromMinutes(30), + EnableMultipleHttp2Connections = true, + // 连接超时5分钟 + ConnectTimeout = TimeSpan.FromMinutes(5), + MaxAutomaticRedirections = 3, + AllowAutoRedirect = true, + Expect100ContinueTimeout = TimeSpan.FromMinutes(30), + }) + { + Timeout = TimeSpan.FromMinutes(30), + DefaultRequestHeaders = + { + { "User-Agent", "yxai" }, + } + }); + } + + return clients; + })).Value[new Random().Next(0, PoolSize)]; + } +} diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiChat/IChatService.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiGateWay/IChatCompletionService.cs similarity index 50% rename from Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiChat/IChatService.cs rename to Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiGateWay/IChatCompletionService.cs index fa023149..ff7f1b01 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiChat/IChatService.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiGateWay/IChatCompletionService.cs @@ -1,27 +1,29 @@ -using OpenAI.Chat; +using Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi; using Yi.Framework.AiHub.Domain.Shared.Dtos; -namespace Yi.Framework.AiHub.Domain.AiChat; +namespace Yi.Framework.AiHub.Domain.AiGateWay; -public interface IChatService +public interface IChatCompletionService { /// /// 聊天完成-流式 /// /// - /// + /// /// /// - public IAsyncEnumerable CompleteChatStreamAsync(AiModelDescribe aiModelDescribe, List messages, + public IAsyncEnumerable CompleteChatStreamAsync(AiModelDescribe aiModelDescribe, + ThorChatCompletionsRequest input, CancellationToken cancellationToken); - + /// /// 聊天完成-非流式 /// /// - /// + /// /// /// - public Task CompleteChatAsync(AiModelDescribe aiModelDescribe, List messages, + public Task CompleteChatAsync(AiModelDescribe aiModelDescribe, + ThorChatCompletionsRequest input, CancellationToken cancellationToken); } \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiGateWay/Impl/ThorAzureDatabricks/Chats/AzureDatabricksChatCompletionsService.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiGateWay/Impl/ThorAzureDatabricks/Chats/AzureDatabricksChatCompletionsService.cs new file mode 100644 index 00000000..fcb53d14 --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiGateWay/Impl/ThorAzureDatabricks/Chats/AzureDatabricksChatCompletionsService.cs @@ -0,0 +1,183 @@ +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.ThorAzureDatabricks.Chats; + +public class AzureDatabricksChatCompletionsService(ILogger logger) + : IChatCompletionService +{ + private string GetAddress(AiModelDescribe? options, string model) + { + // This method should return the appropriate URL for the Azure Databricks API + // based on the provided options and model. + // For now, we will return a placeholder URL. + return $"{options?.Endpoint.TrimEnd('/')}/serving-endpoints/{model}/invocations"; + } + + public async IAsyncEnumerable CompleteChatStreamAsync(AiModelDescribe options, ThorChatCompletionsRequest chatCompletionCreate, + CancellationToken cancellationToken) + { + var address = GetAddress(options, chatCompletionCreate.Model); + using var openai = + Activity.Current?.Source.StartActivity("OpenAI 对话流式补全"); + + chatCompletionCreate.StreamOptions = null; + + var response = await HttpClientFactory.GetHttpClient(address).HttpRequestRaw( + address, + 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) + { + var error = await response.Content.ReadAsStringAsync(); + logger.LogError("OpenAI对话异常 , StatusCode: {StatusCode} 错误响应内容:{Content}", response.StatusCode, + error); + + throw new BusinessException("OpenAI对话异常:" + error, 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(cancellationToken).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); + + if (result == null) + { + continue; + } + + var content = result?.Choices?.FirstOrDefault()?.Delta; + + if (first && content?.Content == OpenAIConstant.ThinkStart) + { + isThink = true; + continue; + // 需要将content的内容转换到其他字段 + } + + if (isThink && content?.Content?.Contains(OpenAIConstant.ThinkEnd) == true) + { + isThink = false; + // 需要将content的内容转换到其他字段 + continue; + } + + if (isThink && result?.Choices != null) + { + // 需要将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) + { + var address = GetAddress(options, chatCompletionCreate.Model); + + using var openai = + Activity.Current?.Source.StartActivity("OpenAI 对话补全"); + + var response = await HttpClientFactory.GetHttpClient(address).PostJsonAsync( + address, + 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/AiGateWay/Impl/ThorAzureOpenAI/AzureOpenAIFactory.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiGateWay/Impl/ThorAzureOpenAI/AzureOpenAIFactory.cs new file mode 100644 index 00000000..330999c7 --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiGateWay/Impl/ThorAzureOpenAI/AzureOpenAIFactory.cs @@ -0,0 +1,71 @@ +using System.ClientModel; +using System.Collections.Concurrent; +using Azure.AI.OpenAI; +using Yi.Framework.AiHub.Domain.Shared.Dtos; + +namespace Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorAzureOpenAI; + +public static class AzureOpenAIFactory +{ + private const string AddressTemplate = "{0}/openai/deployments/{1}/chat/completions?api-version={2}"; + private const string EditImageAddressTemplate = "{0}/openai/deployments/{1}/images/edits?api-version={2}"; + private const string AudioSpeechTemplate = "{0}/openai/deployments/{1}/audio/speech?api-version={2}"; + + private const string AudioTranscriptions = + "{0}/openai/deployments/{1}/audio/transcriptions?api-version={2}"; + + private static readonly ConcurrentDictionary Clients = new(); + + public static string GetAudioTranscriptionsAddress(AiModelDescribe options, string model) + { + if (string.IsNullOrEmpty(options.ExtraUrl)) + { + options.ExtraUrl = "2025-03-01-preview"; + } + + return string.Format(AudioTranscriptions, options.Endpoint.TrimEnd('/'), model, options.ExtraUrl); + } + + public static string GetAudioSpeechAddress(AiModelDescribe options, string model) + { + if (string.IsNullOrEmpty(options.ExtraUrl)) + { + options.ExtraUrl = "2025-03-01-preview"; + } + + return string.Format(AudioSpeechTemplate, options.Endpoint.TrimEnd('/'), model, options.ExtraUrl); + } + + public static string GetAddress(AiModelDescribe options, string model) + { + if (string.IsNullOrEmpty(options.ExtraUrl)) + { + options.ExtraUrl = "2025-03-01-preview"; + } + + return string.Format(AddressTemplate, options.Endpoint.TrimEnd('/'), model, options.ExtraUrl); + } + + public static string GetEditImageAddress(AiModelDescribe options, string model) + { + if (string.IsNullOrEmpty(options.ExtraUrl)) + { + options.ExtraUrl = "2025-03-01-preview"; + } + + return string.Format(EditImageAddressTemplate, options.Endpoint.TrimEnd('/'), model, options.ExtraUrl); + } + + public static AzureOpenAIClient CreateClient(AiModelDescribe options) + { + return Clients.GetOrAdd($"{options.ApiKey}_{options.Endpoint}_{options.ExtraUrl}", (_) => + { + const AzureOpenAIClientOptions.ServiceVersion version = AzureOpenAIClientOptions.ServiceVersion.V2024_06_01; + + var client = new AzureOpenAIClient(new Uri(options.Endpoint), new ApiKeyCredential(options.ApiKey), + new AzureOpenAIClientOptions(version)); + + return client; + }); + } +} \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiGateWay/Impl/ThorAzureOpenAI/Chats/AzureOpenAiChatCompletionCompletionsService.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiGateWay/Impl/ThorAzureOpenAI/Chats/AzureOpenAiChatCompletionCompletionsService.cs new file mode 100644 index 00000000..99367ab4 --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiGateWay/Impl/ThorAzureOpenAI/Chats/AzureOpenAiChatCompletionCompletionsService.cs @@ -0,0 +1,112 @@ +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.ThorAzureOpenAI.Chats; + +public class AzureOpenAiChatCompletionCompletionsService(ILogger logger) + : IChatCompletionService +{ + public async IAsyncEnumerable CompleteChatStreamAsync(AiModelDescribe options, + ThorChatCompletionsRequest chatCompletionCreate, + CancellationToken cancellationToken) + { + using var openai = + Activity.Current?.Source.StartActivity("Azure OpenAI 对话流式补全"); + var url = AzureOpenAIFactory.GetAddress(options, chatCompletionCreate.Model); + + var response = await HttpClientFactory.GetHttpClient(options.Endpoint).HttpRequestRaw(url, + chatCompletionCreate, options.ApiKey, "Api-Key"); + + openai?.SetTag("Model", chatCompletionCreate.Model); + openai?.SetTag("Response", response.StatusCode.ToString()); + + if (response.StatusCode >= HttpStatusCode.BadRequest) + { + var error = await response.Content.ReadAsStringAsync(); + logger.LogError("Azure对话异常 , StatusCode: {StatusCode} 错误响应内容:{Content}", response.StatusCode, + error); + + throw new BusinessException("AzureOpenAI对话异常:" + error, response.StatusCode.ToString()); + } + + using StreamReader reader = new(await response.Content.ReadAsStreamAsync(cancellationToken)); + string? line = string.Empty; + var first = true; + while ((line = await reader.ReadLineAsync().ConfigureAwait(false)) != null) + { + line += Environment.NewLine; + + if (line.StartsWith('{')) + { + logger.LogInformation("AzureOpenAI对话异常 , StatusCode: {StatusCode} Response: {Response}", + response.StatusCode, + line); + + throw new BusinessException("AzureOpenAI对话异常", 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); + + yield return result; + } + } + + public async Task CompleteChatAsync(AiModelDescribe options, + ThorChatCompletionsRequest chatCompletionCreate, + CancellationToken cancellationToken) + { + using var openai = + Activity.Current?.Source.StartActivity("Azure OpenAI 对话补全"); + var url = AzureOpenAIFactory.GetAddress(options, chatCompletionCreate.Model); + + var response = + await HttpClientFactory.GetHttpClient(options.Endpoint) + .PostJsonAsync(url, chatCompletionCreate, options.ApiKey, "Api-Key"); + + openai?.SetTag("Model", chatCompletionCreate.Model); + openai?.SetTag("Response", response.StatusCode.ToString()); + // 如果限流则抛出限流异常 + if (response.StatusCode == HttpStatusCode.TooManyRequests) + { + throw new ThorRateLimitException(); + } + + if (response.StatusCode >= HttpStatusCode.BadRequest) + { + logger.LogError("Azure对话异常 , StatusCode: {StatusCode} Response: {Response} Url:{Url}", response.StatusCode, + await response.Content.ReadAsStringAsync(cancellationToken), url); + } + + var result = await response.Content + .ReadFromJsonAsync(ThorJsonSerializer.DefaultOptions, + 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/AiGateWay/ThorBaseResponse.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiGateWay/ThorBaseResponse.cs new file mode 100644 index 00000000..bbbf4be4 --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiGateWay/ThorBaseResponse.cs @@ -0,0 +1,26 @@ +using System.Text.Json.Serialization; +using Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi; + +namespace Yi.Framework.AiHub.Domain.AiGateWay; + +public record ThorBaseResponse +{ + /// + /// 对象类型 + /// + [JsonPropertyName("object")] + public string? ObjectTypeName { get; set; } + + /// + /// + /// + public bool Successful => Error == null; + + /// + /// + /// + [JsonPropertyName("error")] + public ThorError? Error { get; set; } +} + + diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiGateWay/ThorJsonSerializer.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiGateWay/ThorJsonSerializer.cs new file mode 100644 index 00000000..5ad66fb1 --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiGateWay/ThorJsonSerializer.cs @@ -0,0 +1,14 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Yi.Framework.AiHub.Domain.AiGateWay; + +public static class ThorJsonSerializer +{ + public static JsonSerializerOptions DefaultOptions => new() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + }; +} \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/Chat/MessageAggregateRoot.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/Chat/MessageAggregateRoot.cs index e01d0f05..ddeef24a 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/Chat/MessageAggregateRoot.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/Chat/MessageAggregateRoot.cs @@ -1,6 +1,7 @@ using Mapster; using SqlSugar; using Volo.Abp.Domain.Entities.Auditing; +using Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi; using Yi.Framework.AiHub.Domain.Entities.ValueObjects; using Yi.Framework.AiHub.Domain.Shared.Dtos; using Yi.Framework.AiHub.Domain.Shared.Enums; @@ -19,7 +20,7 @@ public class MessageAggregateRoot : FullAuditedAggregateRoot } public MessageAggregateRoot(Guid userId, Guid? sessionId, string content, string role, string modelId, - TokenUsage? tokenUsage) + ThorUsageResponse? tokenUsage) { UserId = userId; SessionId = sessionId; @@ -28,7 +29,12 @@ public class MessageAggregateRoot : FullAuditedAggregateRoot ModelId = modelId; if (tokenUsage is not null) { - this.TokenUsage = tokenUsage.Adapt(); + this.TokenUsage = new TokenUsageValueObject + { + OutputTokenCount = tokenUsage.OutputTokens ?? 0, + InputTokenCount = tokenUsage.InputTokens ?? 0, + TotalTokenCount = tokenUsage.TotalTokens ?? 0 + }; } this.MessageType = sessionId is null ? MessageTypeEnum.Api : MessageTypeEnum.Web; diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/ValueObjects/TokenUsageValueObject.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/ValueObjects/TokenUsageValueObject.cs index f3a5dcd8..96b3718b 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/ValueObjects/TokenUsageValueObject.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/ValueObjects/TokenUsageValueObject.cs @@ -6,5 +6,5 @@ public class TokenUsageValueObject public int InputTokenCount { get; set; } - public int TotalTokenCount { get; set; } + public long TotalTokenCount { get; set; } } \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Extensions/TimeExtensions.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Extensions/TimeExtensions.cs new file mode 100644 index 00000000..32baf460 --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Extensions/TimeExtensions.cs @@ -0,0 +1,19 @@ +namespace Yi.Framework.AiHub.Domain.Extensions; + +public static class TimeExtensions +{ + public static long ToUnixTimeSeconds(this DateTime dateTime) + { + return new DateTimeOffset(dateTime).ToUnixTimeSeconds(); + } + + public static long ToUnixTimeMilliseconds(this DateTime dateTime) + { + return new DateTimeOffset(dateTime).ToUnixTimeMilliseconds(); + } + + public static DateTime FromUnixTimeSeconds(this long seconds) + { + return DateTimeOffset.FromUnixTimeSeconds(seconds).DateTime; + } +} \ 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 a21acd86..e9d32e09 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 @@ -6,16 +6,13 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; -using OpenAI.Chat; using Volo.Abp.Domain.Services; using Yi.Framework.AiHub.Application.Contracts.Dtos; using Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi; -using Yi.Framework.AiHub.Domain.AiChat; -using Yi.Framework.AiHub.Domain.Entities; +using Yi.Framework.AiHub.Domain.AiGateWay; using Yi.Framework.AiHub.Domain.Entities.Model; using Yi.Framework.AiHub.Domain.Shared.Dtos; using Yi.Framework.SqlSugarCore.Abstractions; -using Usage = Yi.Framework.AiHub.Application.Contracts.Dtos.Usage; namespace Yi.Framework.AiHub.Domain.Managers; @@ -70,17 +67,16 @@ public class AiGateWayManager : DomainService /// /// 聊天完成-流式 /// - /// - /// + /// /// /// - public async IAsyncEnumerable CompleteChatStreamAsync(string modelId, - List messages, + public async IAsyncEnumerable CompleteChatStreamAsync( + ThorChatCompletionsRequest request, [EnumeratorCancellation] CancellationToken cancellationToken) { - var modelDescribe = await GetModelAsync(modelId); - var chatService = LazyServiceProvider.GetRequiredKeyedService(modelDescribe.HandlerName); - await foreach (var result in chatService.CompleteChatStreamAsync(modelDescribe, messages, cancellationToken)) + var modelDescribe = await GetModelAsync(request.Model); + var chatService = LazyServiceProvider.GetRequiredKeyedService(modelDescribe.HandlerName); + await foreach (var result in chatService.CompleteChatStreamAsync(modelDescribe, request, cancellationToken)) { yield return result; } @@ -91,14 +87,13 @@ public class AiGateWayManager : DomainService /// 聊天完成-非流式 /// /// - /// - /// + /// /// /// /// /// - public async Task CompleteChatForStatisticsAsync(HttpContext httpContext, string modelId, - List messages, + public async Task CompleteChatForStatisticsAsync(HttpContext httpContext, + ThorChatCompletionsRequest request, Guid? userId = null, Guid? sessionId = null, CancellationToken cancellationToken = default) @@ -107,33 +102,32 @@ public class AiGateWayManager : DomainService // 设置响应头,声明是 json response.ContentType = "application/json; charset=UTF-8"; await using var writer = new StreamWriter(response.Body, Encoding.UTF8, leaveOpen: true); - var modelDescribe = await GetModelAsync(modelId); - var chatService = LazyServiceProvider.GetRequiredKeyedService(modelDescribe.HandlerName); - var data = await chatService.CompleteChatAsync(modelDescribe, messages, cancellationToken); + var modelDescribe = await GetModelAsync(request.Model); + var chatService = LazyServiceProvider.GetRequiredKeyedService(modelDescribe.HandlerName); + var data = await chatService.CompleteChatAsync(modelDescribe, request, cancellationToken); if (userId is not null) { await _aiMessageManager.CreateUserMessageAsync(userId.Value, sessionId, new MessageInputDto { - Content = messages.LastOrDefault().Content.FirstOrDefault()?.Text ?? string.Empty, - ModelId = modelId, - TokenUsage = data.TokenUsage, + Content = request.Messages.LastOrDefault().Content ?? string.Empty, + ModelId = request.Model, + TokenUsage = data.Usage, }); await _aiMessageManager.CreateSystemMessageAsync(userId.Value, sessionId, new MessageInputDto { - Content = data.Content, - ModelId = modelId, - TokenUsage = data.TokenUsage + Content = data.Choices.FirstOrDefault()?.Delta.Content, + ModelId = request.Model, + TokenUsage = data.Usage }); - await _usageStatisticsManager.SetUsageAsync(userId.Value, modelId, data.TokenUsage.InputTokenCount, - data.TokenUsage.OutputTokenCount); + await _usageStatisticsManager.SetUsageAsync(userId.Value, request.Model, data.Usage.InputTokens ?? 0, + data.Usage.OutputTokens ?? 0); } - var result = MapToChatCompletions(modelId, data.Content); - var body = JsonConvert.SerializeObject(result, new JsonSerializerSettings + var body = JsonConvert.SerializeObject(data, new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() }); @@ -145,16 +139,14 @@ public class AiGateWayManager : DomainService /// 聊天完成-缓存处理 /// /// - /// - /// + /// + /// /// /// - /// /// public async Task CompleteChatStreamForStatisticsAsync( HttpContext httpContext, - string modelId, - List messages, + ThorChatCompletionsRequest request, Guid? userId = null, Guid? sessionId = null, CancellationToken cancellationToken = default) @@ -167,8 +159,8 @@ public class AiGateWayManager : DomainService var gateWay = LazyServiceProvider.GetRequiredService(); - var completeChatResponse = gateWay.CompleteChatStreamAsync(modelId, messages, cancellationToken); - var tokenUsage = new TokenUsage(); + var completeChatResponse = gateWay.CompleteChatStreamAsync(request, cancellationToken); + var tokenUsage = new ThorUsageResponse(); await using var writer = new StreamWriter(response.Body, Encoding.UTF8, leaveOpen: true); //缓存队列算法 @@ -205,17 +197,16 @@ public class AiGateWayManager : DomainService { await foreach (var data in completeChatResponse) { - if (data.IsFinish) + if (data.Usage is not null && data.Usage.TotalTokens is not null) { - tokenUsage = data.TokenUsage; + tokenUsage = data.Usage; } - var model = MapToMessage(modelId, data.Content); - var message = JsonConvert.SerializeObject(model, new JsonSerializerSettings + var message = JsonConvert.SerializeObject(data, new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() }); - backupSystemContent.Append(data.Content); + backupSystemContent.Append(data.Choices.FirstOrDefault()?.Delta.Content); // 将消息加入队列而不是直接写入 messageQueue.Enqueue($"data: {message}\n"); } @@ -223,8 +214,20 @@ public class AiGateWayManager : DomainService catch (Exception e) { _logger.LogError(e, $"Ai对话异常"); - var errorContent = $"Ai对话异常,异常信息:\n{e.Message}"; - var model = MapToMessage(modelId, errorContent); + var errorContent = $"Ai异常,异常信息:\n当前Ai模型:{request.Model}\n异常信息:{e.Message}"; + var model = new ThorChatCompletionsResponse() + { + Choices = new List() + { + new ThorChatChoiceResponse() + { + Delta = new ThorChatMessage() + { + Content = errorContent + } + } + } + }; var message = JsonConvert.SerializeObject(model, new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() @@ -245,8 +248,8 @@ public class AiGateWayManager : DomainService await _aiMessageManager.CreateUserMessageAsync(userId.Value, sessionId, new MessageInputDto { - Content = messages.LastOrDefault().Content.FirstOrDefault()?.Text ?? string.Empty, - ModelId = modelId, + Content = request.Messages.LastOrDefault()?.Content ?? string.Empty, + ModelId = request.Model, TokenUsage = tokenUsage, }); @@ -254,162 +257,12 @@ public class AiGateWayManager : DomainService new MessageInputDto { Content = backupSystemContent.ToString(), - ModelId = modelId, + ModelId = request.Model, TokenUsage = tokenUsage }); - await _usageStatisticsManager.SetUsageAsync(userId.Value, modelId, tokenUsage.InputTokenCount, - tokenUsage.OutputTokenCount); + await _usageStatisticsManager.SetUsageAsync(userId.Value, request.Model, tokenUsage.InputTokens ?? 0, + tokenUsage.OutputTokens ?? 0); } } - - - private SendMessageStreamOutputDto MapToMessage(string modelId, string content) - { - var output = new SendMessageStreamOutputDto - { - Id = "chatcmpl-BotYP3BlN5T4g9YPnW0fBSBvKzXdd", - Object = "chat.completion.chunk", - Created = 1750336171, - Model = modelId, - Choices = new() - { - new Choice - { - Index = 0, - Delta = new Delta - { - Content = content, - Role = "assistant" - }, - FinishReason = null, - ContentFilterResults = new() - { - Hate = new() - { - Filtered = false, - Detected = null - }, - SelfHarm = new() - { - Filtered = false, - Detected = null - }, - Sexual = new() - { - Filtered = false, - Detected = null - }, - Violence = new() - { - Filtered = false, - Detected = null - }, - Jailbreak = new() - { - Filtered = false, - Detected = false - }, - Profanity = new() - { - Filtered = false, - Detected = false - }, - } - } - }, - SystemFingerprint = "", - Usage = new Usage - { - PromptTokens = 0, - CompletionTokens = 0, - TotalTokens = 0, - PromptTokensDetails = new() - { - AudioTokens = 0, - CachedTokens = 0 - }, - CompletionTokensDetails = new() - { - AudioTokens = 0, - ReasoningTokens = 0, - AcceptedPredictionTokens = 0, - RejectedPredictionTokens = 0 - } - } - }; - - return output; - } - - private ChatCompletionsOutput MapToChatCompletions(string modelId, string content) - { - return new ChatCompletionsOutput - { - Id = "resp_67ccd2bed1ec8190b14f964abc0542670bb6a6b452d3795b", - Object = "response", - CreatedAt = 1741476542, - Status = "completed", - Error = null, - IncompleteDetails = null, - Instructions = null, - MaxOutputTokens = null, - Model = modelId, - Output = new List() - { - new Output - { - Type = "message", - Id = "msg_67ccd2bf17f0819081ff3bb2cf6508e60bb6a6b452d3795b", - Status = "completed", - Role = "assistant", - Content = new List - { - new Content - { - Type = "output_text", - Text = content, - Annotations = new List() - } - } - } - }, - ParallelToolCalls = true, - PreviousResponseId = null, - Reasoning = new Reasoning - { - Effort = null, - Summary = null - }, - Store = true, - Temperature = 0, - Text = new Text - { - Format = new Format - { - Type = "text" - } - }, - ToolChoice = "auto", - Tools = new List(), - TopP = 1.0, - Truncation = "disabled", - Usage = new Application.Contracts.Dtos.OpenAi.Usage - { - InputTokens = 0, - InputTokensDetails = new InputTokensDetails - { - CachedTokens = 0 - }, - OutputTokens = 0, - OutputTokensDetails = new OutputTokensDetails - { - ReasoningTokens = 0 - }, - TotalTokens = 0 - }, - User = null, - Metadata = null - }; - } } \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/UsageStatisticsManager.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/UsageStatisticsManager.cs index 2d294ff3..a1782308 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/UsageStatisticsManager.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/UsageStatisticsManager.cs @@ -3,6 +3,8 @@ using Volo.Abp.Domain.Services; using Yi.Framework.AiHub.Domain.Entities; using Yi.Framework.SqlSugarCore.Abstractions; +namespace Yi.Framework.AiHub.Domain.Managers; + public class UsageStatisticsManager : DomainService { private readonly ISqlSugarRepository _repository; 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 17b15647..48f2995a 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 @@ -1,7 +1,8 @@ using Microsoft.Extensions.DependencyInjection; using Volo.Abp.Domain; -using Yi.Framework.AiHub.Domain.AiChat; -using Yi.Framework.AiHub.Domain.AiChat.Impl; +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.Shared; using Yi.Framework.Mapster; @@ -20,9 +21,10 @@ namespace Yi.Framework.AiHub.Domain var services = context.Services; // Configure(configuration.GetSection("AiGateWay")); - // - services.AddKeyedTransient(nameof(AzureChatService)); - services.AddKeyedTransient(nameof(AzureRestChatService)); + services.AddKeyedTransient( + nameof(AzureOpenAiChatCompletionCompletionsService)); + services.AddKeyedTransient( + nameof(AzureDatabricksChatCompletionsService)); } public override async Task OnApplicationInitializationAsync(ApplicationInitializationContext context)