feat: 新增gemini支持

This commit is contained in:
chenchun
2025-12-17 18:47:28 +08:00
parent 340e2016d6
commit 4e421c160c
9 changed files with 285 additions and 9 deletions

View File

@@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Http;
using System.Text.Json;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Volo.Abp.Application.Services;
@@ -201,7 +202,7 @@ public class OpenApiService : ApplicationService
}
}
/// <summary>
/// 响应-Openai新规范 (尊享服务专用)
/// </summary>
@@ -240,9 +241,9 @@ public class OpenApiService : ApplicationService
//ai网关代理httpcontext
if (input.Stream == true)
{
await _aiGateWayManager.OpenAiResponsesStreamForStatisticsAsync(_httpContextAccessor.HttpContext,
input,
userId, null, tokenId, cancellationToken);
await _aiGateWayManager.OpenAiResponsesStreamForStatisticsAsync(_httpContextAccessor.HttpContext,
input,
userId, null, tokenId, cancellationToken);
}
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
private string? GetTokenByHttpContext(HttpContext httpContext)

View File

@@ -11,6 +11,8 @@ public class PremiumPackageConst
"claude-opus-4-5-20251101",
"gemini-3-pro-preview",
"gpt-5.1-codex-max",
"gpt-5.2"
"gpt-5.2",
"gemini-3-pro-high",
"gemini-3-pro-image-preview"
];
}

View File

@@ -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,
};
}
}

View File

@@ -11,5 +11,8 @@ public enum ModelApiTypeEnum
Claude,
[Description("Response")]
Response
Response,
[Description("GenerateContent")]
GenerateContent
}

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}
}

View File

@@ -16,6 +16,7 @@ using Yi.Framework.AiHub.Domain.Entities.Model;
using Yi.Framework.AiHub.Domain.Shared.Consts;
using Yi.Framework.AiHub.Domain.Shared.Dtos;
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.Embeddings;
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响应
private static readonly byte[] EventPrefix = "event: "u8.ToArray();

View File

@@ -17,6 +17,7 @@ using Yi.Framework.AiHub.Domain.Shared;
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
using Yi.Framework.Mapster;
using Microsoft.Extensions.DependencyInjection;
using Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorGemini.Chats;
namespace Yi.Framework.AiHub.Domain
{
@@ -61,6 +62,12 @@ namespace Yi.Framework.AiHub.Domain
#endregion
#region Gemini GenerateContent
services.AddKeyedTransient<IGeminiGenerateContentService, GeminiGenerateContentService>(
nameof(GeminiGenerateContentService));
#endregion
#region Image
services.AddKeyedTransient<IImageService, AzureOpenAIServiceImageService>(