feat: 支持非流式传输
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Net.Http.Json;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
@@ -62,7 +63,8 @@ public class AzureRestChatService : IChatService
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
throw new UserFriendlyException($"当前模型不可用:{aiModelDescribe.ModelId},状态码:{response.StatusCode},原因:{response.ReasonPhrase}");
|
||||
throw new UserFriendlyException(
|
||||
$"当前模型不可用:{aiModelDescribe.ModelId},状态码:{response.StatusCode},原因:{response.ReasonPhrase}");
|
||||
}
|
||||
// 确认响应成功
|
||||
// response.EnsureSuccessStatusCode();
|
||||
@@ -76,11 +78,11 @@ public class AzureRestChatService : IChatService
|
||||
var result = new CompleteChatResponse();
|
||||
try
|
||||
{
|
||||
var jsonObj = MapToJObject(line);
|
||||
var jsonObj = MapToStreamJObject(line);
|
||||
if (jsonObj is not null)
|
||||
{
|
||||
var content = GetContent(jsonObj);
|
||||
var tokenUsage = GetTokenUsage(jsonObj);
|
||||
var content = GetStreamContent(jsonObj);
|
||||
var tokenUsage = GetStreamTokenUsage(jsonObj);
|
||||
result = new CompleteChatResponse
|
||||
{
|
||||
TokenUsage = tokenUsage,
|
||||
@@ -98,28 +100,85 @@ public class AzureRestChatService : IChatService
|
||||
}
|
||||
}
|
||||
|
||||
public Task<CompleteChatResponse> CompleteChatAsync(AiModelDescribe aiModelDescribe, List<ChatMessage> messages, CancellationToken cancellationToken)
|
||||
public async Task<CompleteChatResponse> CompleteChatAsync(AiModelDescribe aiModelDescribe,
|
||||
List<ChatMessage> messages, CancellationToken cancellationToken)
|
||||
{
|
||||
throw new NotImplementedException("暂未实现");
|
||||
// 设置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 line)
|
||||
private JObject? MapToJObject(string body)
|
||||
{
|
||||
if (line == "data: [DONE]"||string.IsNullOrWhiteSpace(line) )
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(body))
|
||||
return null;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(line))
|
||||
return null;
|
||||
string prefix = "data: ";
|
||||
line = line.Substring(prefix.Length);
|
||||
return JObject.Parse(line);
|
||||
return JObject.Parse(body);
|
||||
}
|
||||
|
||||
private string? GetContent(JObject? jsonObj)
|
||||
{
|
||||
var contentToken = jsonObj.SelectToken("choices[0].delta.content");
|
||||
var contentToken = jsonObj.SelectToken("choices[0].message.content");
|
||||
if (contentToken != null && contentToken.Type != JTokenType.Null)
|
||||
{
|
||||
return contentToken.ToString();
|
||||
@@ -157,4 +216,60 @@ public class AzureRestChatService : IChatService
|
||||
|
||||
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<int>();
|
||||
}
|
||||
|
||||
var promptTokens = usage["prompt_tokens"];
|
||||
if (promptTokens is not null && promptTokens.Type != JTokenType.Null)
|
||||
{
|
||||
result.InputTokenCount = promptTokens.ToObject<int>();
|
||||
}
|
||||
|
||||
var totalTokens = usage["total_tokens"];
|
||||
if (totalTokens is not null && totalTokens.Type != JTokenType.Null)
|
||||
{
|
||||
result.TotalTokenCount = totalTokens.ToObject<int>();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -9,11 +9,13 @@ 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.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;
|
||||
|
||||
@@ -107,7 +109,7 @@ public class AiGateWayManager : DomainService
|
||||
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 output = await chatService.CompleteChatAsync(modelDescribe, messages, cancellationToken);
|
||||
var data = await chatService.CompleteChatAsync(modelDescribe, messages, cancellationToken);
|
||||
if (userId is not null)
|
||||
{
|
||||
await _aiMessageManager.CreateUserMessageAsync(userId.Value, sessionId,
|
||||
@@ -115,22 +117,23 @@ public class AiGateWayManager : DomainService
|
||||
{
|
||||
Content = messages.LastOrDefault().Content.FirstOrDefault()?.Text ?? string.Empty,
|
||||
ModelId = modelId,
|
||||
TokenUsage = output.TokenUsage,
|
||||
TokenUsage = data.TokenUsage,
|
||||
});
|
||||
|
||||
await _aiMessageManager.CreateSystemMessageAsync(userId.Value, sessionId,
|
||||
new MessageInputDto
|
||||
{
|
||||
Content = output.Content,
|
||||
Content = data.Content,
|
||||
ModelId = modelId,
|
||||
TokenUsage = output.TokenUsage
|
||||
TokenUsage = data.TokenUsage
|
||||
});
|
||||
|
||||
await _usageStatisticsManager.SetUsageAsync(userId.Value, modelId, output.TokenUsage.InputTokenCount,
|
||||
output.TokenUsage.OutputTokenCount);
|
||||
await _usageStatisticsManager.SetUsageAsync(userId.Value, modelId, data.TokenUsage.InputTokenCount,
|
||||
data.TokenUsage.OutputTokenCount);
|
||||
}
|
||||
|
||||
var body = JsonConvert.SerializeObject(output, new JsonSerializerSettings
|
||||
var result = MapToChatCompletions(modelId, data.Content);
|
||||
var body = JsonConvert.SerializeObject(result, new JsonSerializerSettings
|
||||
{
|
||||
ContractResolver = new CamelCasePropertyNamesContractResolver()
|
||||
});
|
||||
@@ -261,9 +264,9 @@ public class AiGateWayManager : DomainService
|
||||
}
|
||||
|
||||
|
||||
private SendMessageOutputDto MapToMessage(string modelId, string content)
|
||||
private SendMessageStreamOutputDto MapToMessage(string modelId, string content)
|
||||
{
|
||||
var output = new SendMessageOutputDto
|
||||
var output = new SendMessageStreamOutputDto
|
||||
{
|
||||
Id = "chatcmpl-BotYP3BlN5T4g9YPnW0fBSBvKzXdd",
|
||||
Object = "chat.completion.chunk",
|
||||
@@ -338,4 +341,75 @@ public class AiGateWayManager : DomainService
|
||||
|
||||
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
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user