mirror of
https://gitee.com/ccnetcore/Yi
synced 2026-03-19 16:06:36 +08:00
feat: Thor搭建
This commit is contained in:
@@ -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<string, AzureOpenAIClient> 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;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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<AzureOpenAiChatCompletionCompletionsService> logger)
|
||||
: IChatCompletionService
|
||||
{
|
||||
public async IAsyncEnumerable<ThorChatCompletionsResponse> 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<ThorChatCompletionsResponse>(line,
|
||||
ThorJsonSerializer.DefaultOptions);
|
||||
|
||||
yield return result;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ThorChatCompletionsResponse> 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<ThorChatCompletionsResponse>(ThorJsonSerializer.DefaultOptions,
|
||||
cancellationToken: cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user