diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/OpenApiService.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/OpenApiService.cs
index 3649e5ad..ff4b46d8 100644
--- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/OpenApiService.cs
+++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/OpenApiService.cs
@@ -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
}
}
-
+
///
/// 响应-Openai新规范 (尊享服务专用)
///
@@ -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
}
}
-
+
+ ///
+ /// 生成-Gemini (尊享服务专用)
+ ///
+ ///
+ ///
+ ///
+ ///
+ [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)
diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Consts/PremiumPackageConst.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Consts/PremiumPackageConst.cs
index 0fffa52a..5e1e56e4 100644
--- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Consts/PremiumPackageConst.cs
+++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Consts/PremiumPackageConst.cs
@@ -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"
];
}
\ No newline at end of file
diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Dtos/Gemini/GeminiGenerateContentAcquirer.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Dtos/Gemini/GeminiGenerateContentAcquirer.cs
new file mode 100644
index 00000000..8695d4b4
--- /dev/null
+++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Dtos/Gemini/GeminiGenerateContentAcquirer.cs
@@ -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,
+ };
+ }
+}
\ No newline at end of file
diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Enums/ModelApiTypeEnum.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Enums/ModelApiTypeEnum.cs
index 5b407f60..3ce86e09 100644
--- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Enums/ModelApiTypeEnum.cs
+++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Enums/ModelApiTypeEnum.cs
@@ -11,5 +11,8 @@ public enum ModelApiTypeEnum
Claude,
[Description("Response")]
- Response
+ Response,
+
+ [Description("GenerateContent")]
+ GenerateContent
}
\ No newline at end of file
diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiGateWay/IGeminiGenerateContentService.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiGateWay/IGeminiGenerateContentService.cs
new file mode 100644
index 00000000..af890035
--- /dev/null
+++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiGateWay/IGeminiGenerateContentService.cs
@@ -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
+{
+ ///
+ /// 聊天完成-流式
+ ///
+ ///
+ ///
+ ///
+ ///
+ public IAsyncEnumerable GenerateContentStreamAsync(AiModelDescribe aiModelDescribe,
+ JsonElement input,
+ CancellationToken cancellationToken);
+
+ ///
+ /// 聊天完成-非流式
+ ///
+ ///
+ ///
+ ///
+ ///
+ public Task GenerateContentAsync(AiModelDescribe aiModelDescribe,
+ JsonElement input,
+ CancellationToken cancellationToken);
+}
\ No newline at end of file
diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiGateWay/Impl/ThorGemini/Chats/GeminiGenerateContentService.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiGateWay/Impl/ThorGemini/Chats/GeminiGenerateContentService.cs
new file mode 100644
index 00000000..6f1a0831
--- /dev/null
+++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiGateWay/Impl/ThorGemini/Chats/GeminiGenerateContentService.cs
@@ -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 logger,IHttpClientFactory httpClientFactory):IGeminiGenerateContentService
+{
+ public IAsyncEnumerable GenerateContentStreamAsync(AiModelDescribe aiModelDescribe, JsonElement input,
+ CancellationToken cancellationToken)
+ {
+ throw new NotImplementedException();
+ }
+
+ public async Task 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()
+ {
+ {"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(
+ cancellationToken: cancellationToken).ConfigureAwait(false);
+
+ return result;
+ }
+}
\ No newline at end of file
diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiGateWay/SupplementalMultiplierHelper.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiGateWay/SupplementalMultiplierHelper.cs
new file mode 100644
index 00000000..056ef99b
--- /dev/null
+++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiGateWay/SupplementalMultiplierHelper.cs
@@ -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);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/AiGateWayManager.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/AiGateWayManager.cs
index 021913a2..a9b48196 100644
--- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/AiGateWayManager.cs
+++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/AiGateWayManager.cs
@@ -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
}
}
}
-
+
+ ///
+ /// Gemini 生成-非流式-缓存处理
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ 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(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();
diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/YiFrameworkAiHubDomainModule.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/YiFrameworkAiHubDomainModule.cs
index 4c3c25ce..72c073c7 100644
--- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/YiFrameworkAiHubDomainModule.cs
+++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/YiFrameworkAiHubDomainModule.cs
@@ -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(
+ nameof(GeminiGenerateContentService));
+
+ #endregion
+
#region Image
services.AddKeyedTransient(