feat: Thor搭建

This commit is contained in:
ccnetcore
2025-07-17 23:10:26 +08:00
parent 10f7499066
commit e593f2cba4
54 changed files with 2526 additions and 867 deletions

View File

@@ -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
/// <summary>
/// 聊天完成-流式
/// </summary>
/// <param name="modelId"></param>
/// <param name="messages"></param>
/// <param name="request"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async IAsyncEnumerable<CompleteChatResponse> CompleteChatStreamAsync(string modelId,
List<ChatMessage> messages,
public async IAsyncEnumerable<ThorChatCompletionsResponse> CompleteChatStreamAsync(
ThorChatCompletionsRequest request,
[EnumeratorCancellation] CancellationToken cancellationToken)
{
var modelDescribe = await GetModelAsync(modelId);
var chatService = LazyServiceProvider.GetRequiredKeyedService<IChatService>(modelDescribe.HandlerName);
await foreach (var result in chatService.CompleteChatStreamAsync(modelDescribe, messages, cancellationToken))
var modelDescribe = await GetModelAsync(request.Model);
var chatService = LazyServiceProvider.GetRequiredKeyedService<IChatCompletionService>(modelDescribe.HandlerName);
await foreach (var result in chatService.CompleteChatStreamAsync(modelDescribe, request, cancellationToken))
{
yield return result;
}
@@ -91,14 +87,13 @@ public class AiGateWayManager : DomainService
/// 聊天完成-非流式
/// </summary>
/// <param name="httpContext"></param>
/// <param name="modelId"></param>
/// <param name="messages"></param>
/// <param name="request"></param>
/// <param name="userId"></param>
/// <param name="sessionId"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task CompleteChatForStatisticsAsync(HttpContext httpContext, string modelId,
List<ChatMessage> 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<IChatService>(modelDescribe.HandlerName);
var data = await chatService.CompleteChatAsync(modelDescribe, messages, cancellationToken);
var modelDescribe = await GetModelAsync(request.Model);
var chatService = LazyServiceProvider.GetRequiredKeyedService<IChatCompletionService>(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
/// 聊天完成-缓存处理
/// </summary>
/// <param name="httpContext"></param>
/// <param name="modelId"></param>
/// <param name="messages"></param>
/// <param name="request"></param>
/// <param name="userId"></param>
/// <param name="sessionId"></param>
/// <param name="cancellationToken"></param>
/// <param name="userId"></param>
/// <returns></returns>
public async Task CompleteChatStreamForStatisticsAsync(
HttpContext httpContext,
string modelId,
List<ChatMessage> messages,
ThorChatCompletionsRequest request,
Guid? userId = null,
Guid? sessionId = null,
CancellationToken cancellationToken = default)
@@ -167,8 +159,8 @@ public class AiGateWayManager : DomainService
var gateWay = LazyServiceProvider.GetRequiredService<AiGateWayManager>();
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<ThorChatChoiceResponse>()
{
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<Output>()
{
new Output
{
Type = "message",
Id = "msg_67ccd2bf17f0819081ff3bb2cf6508e60bb6a6b452d3795b",
Status = "completed",
Role = "assistant",
Content = new List<Content>
{
new Content
{
Type = "output_text",
Text = content,
Annotations = new List<object>()
}
}
}
},
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<object>(),
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
};
}
}

View File

@@ -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<UsageStatisticsAggregateRoot> _repository;