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(