feat: 新增gemini支持
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
using Microsoft.AspNetCore.Http;
|
using System.Text.Json;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Volo.Abp.Application.Services;
|
using Volo.Abp.Application.Services;
|
||||||
@@ -201,7 +202,7 @@ public class OpenApiService : ApplicationService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 响应-Openai新规范 (尊享服务专用)
|
/// 响应-Openai新规范 (尊享服务专用)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -240,9 +241,9 @@ public class OpenApiService : ApplicationService
|
|||||||
//ai网关代理httpcontext
|
//ai网关代理httpcontext
|
||||||
if (input.Stream == true)
|
if (input.Stream == true)
|
||||||
{
|
{
|
||||||
await _aiGateWayManager.OpenAiResponsesStreamForStatisticsAsync(_httpContextAccessor.HttpContext,
|
await _aiGateWayManager.OpenAiResponsesStreamForStatisticsAsync(_httpContextAccessor.HttpContext,
|
||||||
input,
|
input,
|
||||||
userId, null, tokenId, cancellationToken);
|
userId, null, tokenId, cancellationToken);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -253,7 +254,63 @@ public class OpenApiService : ApplicationService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 生成-Gemini (尊享服务专用)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="input"></param>
|
||||||
|
/// <param name="modelId"></param>
|
||||||
|
/// <param name="alt"></param>
|
||||||
|
/// <param name="cancellationToken"></param>
|
||||||
|
[HttpPost("openApi/v1beta/models/{modelId}:generateContent")]
|
||||||
|
public async Task GenerateContentAsync([FromBody] JsonElement input,
|
||||||
|
[FromRoute] string modelId,
|
||||||
|
[FromQuery] string? alt, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
//前面都是校验,后面才是真正的调用
|
||||||
|
var httpContext = this._httpContextAccessor.HttpContext;
|
||||||
|
var tokenValidation = await _tokenManager.ValidateTokenAsync(GetTokenByHttpContext(httpContext), modelId);
|
||||||
|
var userId = tokenValidation.UserId;
|
||||||
|
var tokenId = tokenValidation.TokenId;
|
||||||
|
await _aiBlacklistManager.VerifiyAiBlacklist(userId);
|
||||||
|
|
||||||
|
// 验证用户是否为VIP
|
||||||
|
var userInfo = await _accountService.GetAsync(null, null, userId);
|
||||||
|
if (userInfo == null)
|
||||||
|
{
|
||||||
|
throw new UserFriendlyException("用户信息不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否为VIP(使用RoleCodes判断)
|
||||||
|
if (!userInfo.RoleCodes.Contains(AiHubConst.VipRole) && userInfo.User.UserName != "cc")
|
||||||
|
{
|
||||||
|
throw new UserFriendlyException("该接口为尊享服务专用,需要VIP权限才能使用");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查尊享token包用量
|
||||||
|
var availableTokens = await _premiumPackageManager.GetAvailableTokensAsync(userId);
|
||||||
|
if (availableTokens <= 0)
|
||||||
|
{
|
||||||
|
throw new UserFriendlyException("尊享token包用量不足,请先购买尊享token包");
|
||||||
|
}
|
||||||
|
|
||||||
|
//ai网关代理httpcontext
|
||||||
|
if (alt == "sse")
|
||||||
|
{
|
||||||
|
// await _aiGateWayManager.OpenAiResponsesStreamForStatisticsAsync(_httpContextAccessor.HttpContext,
|
||||||
|
// input,
|
||||||
|
// userId, null, tokenId, cancellationToken);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await _aiGateWayManager.GeminiGenerateContentAsyncForStatisticsAsync(_httpContextAccessor.HttpContext,
|
||||||
|
modelId, input,
|
||||||
|
userId,
|
||||||
|
null, tokenId,
|
||||||
|
cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#region 私有
|
#region 私有
|
||||||
|
|
||||||
private string? GetTokenByHttpContext(HttpContext httpContext)
|
private string? GetTokenByHttpContext(HttpContext httpContext)
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ public class PremiumPackageConst
|
|||||||
"claude-opus-4-5-20251101",
|
"claude-opus-4-5-20251101",
|
||||||
"gemini-3-pro-preview",
|
"gemini-3-pro-preview",
|
||||||
"gpt-5.1-codex-max",
|
"gpt-5.1-codex-max",
|
||||||
"gpt-5.2"
|
"gpt-5.2",
|
||||||
|
"gemini-3-pro-high",
|
||||||
|
"gemini-3-pro-image-preview"
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
|
||||||
|
using Yi.Framework.AiHub.Domain.Shared.Extensions;
|
||||||
|
|
||||||
|
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.Gemini;
|
||||||
|
|
||||||
|
public static class GeminiGenerateContentAcquirer
|
||||||
|
{
|
||||||
|
public static ThorUsageResponse GetUsage(JsonElement response)
|
||||||
|
{
|
||||||
|
var usage = response.GetPath("usageMetadata");
|
||||||
|
if (!usage.HasValue)
|
||||||
|
{
|
||||||
|
return new ThorUsageResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
var inputTokens = usage.Value.GetPath("promptTokenCount").GetInt();
|
||||||
|
var outputTokens = usage.Value.GetPath("candidatesTokenCount").GetInt()
|
||||||
|
+ usage.Value.GetPath("cachedContentTokenCount").GetInt()
|
||||||
|
+ usage.Value.GetPath("thoughtsTokenCount").GetInt()
|
||||||
|
+ usage.Value.GetPath("toolUsePromptTokenCount").GetInt();
|
||||||
|
|
||||||
|
|
||||||
|
return new ThorUsageResponse
|
||||||
|
{
|
||||||
|
PromptTokens = inputTokens,
|
||||||
|
InputTokens = inputTokens,
|
||||||
|
OutputTokens = outputTokens,
|
||||||
|
CompletionTokens = outputTokens,
|
||||||
|
TotalTokens = inputTokens + outputTokens,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,5 +11,8 @@ public enum ModelApiTypeEnum
|
|||||||
Claude,
|
Claude,
|
||||||
|
|
||||||
[Description("Response")]
|
[Description("Response")]
|
||||||
Response
|
Response,
|
||||||
|
|
||||||
|
[Description("GenerateContent")]
|
||||||
|
GenerateContent
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using Yi.Framework.AiHub.Domain.Shared.Dtos;
|
||||||
|
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
|
||||||
|
|
||||||
|
namespace Yi.Framework.AiHub.Domain.AiGateWay;
|
||||||
|
|
||||||
|
public interface IGeminiGenerateContentService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 聊天完成-流式
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="aiModelDescribe"></param>
|
||||||
|
/// <param name="input"></param>
|
||||||
|
/// <param name="cancellationToken"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public IAsyncEnumerable<JsonElement?> GenerateContentStreamAsync(AiModelDescribe aiModelDescribe,
|
||||||
|
JsonElement input,
|
||||||
|
CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 聊天完成-非流式
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="aiModelDescribe"></param>
|
||||||
|
/// <param name="input"></param>
|
||||||
|
/// <param name="cancellationToken"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public Task<JsonElement> GenerateContentAsync(AiModelDescribe aiModelDescribe,
|
||||||
|
JsonElement input,
|
||||||
|
CancellationToken cancellationToken);
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Http.Json;
|
||||||
|
using System.Text.Json;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Yi.Framework.AiHub.Domain.AiGateWay.Exceptions;
|
||||||
|
using Yi.Framework.AiHub.Domain.Shared.Dtos;
|
||||||
|
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi.Responses;
|
||||||
|
|
||||||
|
namespace Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorGemini.Chats;
|
||||||
|
|
||||||
|
public class GeminiGenerateContentService(ILogger<GeminiGenerateContentService> logger,IHttpClientFactory httpClientFactory):IGeminiGenerateContentService
|
||||||
|
{
|
||||||
|
public IAsyncEnumerable<JsonElement?> GenerateContentStreamAsync(AiModelDescribe aiModelDescribe, JsonElement input,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<JsonElement> GenerateContentAsync(AiModelDescribe options,JsonElement input, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var response = await httpClientFactory.CreateClient().PostJsonAsync(
|
||||||
|
options?.Endpoint.TrimEnd('/') + $"/v1beta/models/{options.ModelId}:generateContent",
|
||||||
|
input,null, new Dictionary<string,string>()
|
||||||
|
{
|
||||||
|
{"x-goog-api-key",options.ApiKey}
|
||||||
|
}).ConfigureAwait(false);
|
||||||
|
|
||||||
|
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("Gemini 生成异常 请求地址:{Address}, StatusCode: {StatusCode} Response: {Response}", options.Endpoint,
|
||||||
|
response.StatusCode, error);
|
||||||
|
|
||||||
|
throw new BusinessException("Gemini 生成异常", response.StatusCode.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
var result =
|
||||||
|
await response.Content.ReadFromJsonAsync<JsonElement>(
|
||||||
|
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
|
||||||
|
|
||||||
|
namespace Yi.Framework.AiHub.Domain.AiGateWay;
|
||||||
|
|
||||||
|
public static class SupplementalMultiplierHelper
|
||||||
|
{
|
||||||
|
public static void SetSupplementalMultiplier(this ThorUsageResponse? usage,decimal multiplier)
|
||||||
|
{
|
||||||
|
if (usage is not null)
|
||||||
|
{
|
||||||
|
usage.InputTokens =
|
||||||
|
(int)Math.Round((usage.InputTokens ?? 0) * multiplier);
|
||||||
|
usage.OutputTokens =
|
||||||
|
(int)Math.Round((usage.OutputTokens ?? 0) * multiplier);
|
||||||
|
usage.CompletionTokens =
|
||||||
|
(int)Math.Round((usage.CompletionTokens ?? 0) * multiplier);
|
||||||
|
usage.PromptTokens =
|
||||||
|
(int)Math.Round((usage.PromptTokens ?? 0) * multiplier);
|
||||||
|
usage.TotalTokens =
|
||||||
|
(int)Math.Round((usage.TotalTokens ?? 0) * multiplier);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ using Yi.Framework.AiHub.Domain.Entities.Model;
|
|||||||
using Yi.Framework.AiHub.Domain.Shared.Consts;
|
using Yi.Framework.AiHub.Domain.Shared.Consts;
|
||||||
using Yi.Framework.AiHub.Domain.Shared.Dtos;
|
using Yi.Framework.AiHub.Domain.Shared.Dtos;
|
||||||
using Yi.Framework.AiHub.Domain.Shared.Dtos.Anthropic;
|
using Yi.Framework.AiHub.Domain.Shared.Dtos.Anthropic;
|
||||||
|
using Yi.Framework.AiHub.Domain.Shared.Dtos.Gemini;
|
||||||
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
|
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
|
||||||
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi.Embeddings;
|
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi.Embeddings;
|
||||||
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi.Images;
|
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi.Images;
|
||||||
@@ -786,8 +787,72 @@ public class AiGateWayManager : DomainService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gemini 生成-非流式-缓存处理
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="httpContext"></param>
|
||||||
|
/// <param name="modelId"></param>
|
||||||
|
/// <param name="request"></param>
|
||||||
|
/// <param name="userId"></param>
|
||||||
|
/// <param name="sessionId"></param>
|
||||||
|
/// <param name="tokenId"></param>
|
||||||
|
/// <param name="cancellationToken"></param>
|
||||||
|
public async Task GeminiGenerateContentAsyncForStatisticsAsync(HttpContext httpContext,
|
||||||
|
string modelId,
|
||||||
|
JsonElement request,
|
||||||
|
Guid? userId = null,
|
||||||
|
Guid? sessionId = null,
|
||||||
|
Guid? tokenId = null,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var response = httpContext.Response;
|
||||||
|
var modelDescribe = await GetModelAsync(ModelApiTypeEnum.GenerateContent, modelId);
|
||||||
|
|
||||||
|
var chatService =
|
||||||
|
LazyServiceProvider.GetRequiredKeyedService<IGeminiGenerateContentService>(modelDescribe.HandlerName);
|
||||||
|
var data = await chatService.GenerateContentAsync(modelDescribe, request, cancellationToken);
|
||||||
|
|
||||||
|
var tokenUsage= GeminiGenerateContentAcquirer.GetUsage(data);
|
||||||
|
tokenUsage.SetSupplementalMultiplier(modelDescribe.Multiplier);
|
||||||
|
|
||||||
|
if (userId is not null)
|
||||||
|
{
|
||||||
|
await _aiMessageManager.CreateUserMessageAsync(userId.Value, sessionId,
|
||||||
|
new MessageInputDto
|
||||||
|
{
|
||||||
|
Content = "不予存储",
|
||||||
|
ModelId = modelId,
|
||||||
|
TokenUsage = tokenUsage,
|
||||||
|
}, tokenId);
|
||||||
|
|
||||||
|
await _aiMessageManager.CreateSystemMessageAsync(userId.Value, sessionId,
|
||||||
|
new MessageInputDto
|
||||||
|
{
|
||||||
|
Content = "不予存储",
|
||||||
|
ModelId = modelId,
|
||||||
|
TokenUsage = tokenUsage
|
||||||
|
}, tokenId);
|
||||||
|
|
||||||
|
await _usageStatisticsManager.SetUsageAsync(userId.Value, modelId, tokenUsage, tokenId);
|
||||||
|
|
||||||
|
// 扣减尊享token包用量
|
||||||
|
var totalTokens = tokenUsage.TotalTokens ?? 0;
|
||||||
|
if (totalTokens > 0)
|
||||||
|
{
|
||||||
|
await PremiumPackageManager.TryConsumeTokensAsync(userId.Value, totalTokens);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await response.WriteAsJsonAsync(data, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#region 流式传输格式Http响应
|
#region 流式传输格式Http响应
|
||||||
|
|
||||||
private static readonly byte[] EventPrefix = "event: "u8.ToArray();
|
private static readonly byte[] EventPrefix = "event: "u8.ToArray();
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ using Yi.Framework.AiHub.Domain.Shared;
|
|||||||
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
|
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
|
||||||
using Yi.Framework.Mapster;
|
using Yi.Framework.Mapster;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorGemini.Chats;
|
||||||
|
|
||||||
namespace Yi.Framework.AiHub.Domain
|
namespace Yi.Framework.AiHub.Domain
|
||||||
{
|
{
|
||||||
@@ -61,6 +62,12 @@ namespace Yi.Framework.AiHub.Domain
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region Gemini GenerateContent
|
||||||
|
services.AddKeyedTransient<IGeminiGenerateContentService, GeminiGenerateContentService>(
|
||||||
|
nameof(GeminiGenerateContentService));
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
#region Image
|
#region Image
|
||||||
|
|
||||||
services.AddKeyedTransient<IImageService, AzureOpenAIServiceImageService>(
|
services.AddKeyedTransient<IImageService, AzureOpenAIServiceImageService>(
|
||||||
|
|||||||
Reference in New Issue
Block a user