feat: 全面支持geminicli

This commit is contained in:
ccnetcore
2025-12-17 21:51:01 +08:00
parent 4e421c160c
commit fcf0fd7f70
4 changed files with 152 additions and 19 deletions

View File

@@ -262,7 +262,7 @@ public class OpenApiService : ApplicationService
/// <param name="modelId"></param> /// <param name="modelId"></param>
/// <param name="alt"></param> /// <param name="alt"></param>
/// <param name="cancellationToken"></param> /// <param name="cancellationToken"></param>
[HttpPost("openApi/v1beta/models/{modelId}:generateContent")] [HttpPost("openApi/v1beta/models/{modelId}:{action:regex(^(generateContent|streamGenerateContent)$)}")]
public async Task GenerateContentAsync([FromBody] JsonElement input, public async Task GenerateContentAsync([FromBody] JsonElement input,
[FromRoute] string modelId, [FromRoute] string modelId,
[FromQuery] string? alt, CancellationToken cancellationToken) [FromQuery] string? alt, CancellationToken cancellationToken)
@@ -297,13 +297,15 @@ public class OpenApiService : ApplicationService
//ai网关代理httpcontext //ai网关代理httpcontext
if (alt == "sse") if (alt == "sse")
{ {
// await _aiGateWayManager.OpenAiResponsesStreamForStatisticsAsync(_httpContextAccessor.HttpContext, await _aiGateWayManager.GeminiGenerateContentStreamForStatisticsAsync(_httpContextAccessor.HttpContext,
// input, modelId, input,
// userId, null, tokenId, cancellationToken); userId,
null, tokenId,
cancellationToken);
} }
else else
{ {
await _aiGateWayManager.GeminiGenerateContentAsyncForStatisticsAsync(_httpContextAccessor.HttpContext, await _aiGateWayManager.GeminiGenerateContentForStatisticsAsync(_httpContextAccessor.HttpContext,
modelId, input, modelId, input,
userId, userId,
null, tokenId, null, tokenId,
@@ -321,6 +323,13 @@ public class OpenApiService : ApplicationService
{ {
return apiKeyHeader.Trim(); return apiKeyHeader.Trim();
} }
// 再从 谷歌 获取
string googApiKeyHeader = httpContext.Request.Headers["x-goog-api-key"];
if (!string.IsNullOrWhiteSpace(googApiKeyHeader))
{
return googApiKeyHeader.Trim();
}
// 再检查 Authorization 头 // 再检查 Authorization 头
string authHeader = httpContext.Request.Headers["Authorization"]; string authHeader = httpContext.Request.Headers["Authorization"];

View File

@@ -6,12 +6,12 @@ namespace Yi.Framework.AiHub.Domain.Shared.Dtos.Gemini;
public static class GeminiGenerateContentAcquirer public static class GeminiGenerateContentAcquirer
{ {
public static ThorUsageResponse GetUsage(JsonElement response) public static ThorUsageResponse? GetUsage(JsonElement response)
{ {
var usage = response.GetPath("usageMetadata"); var usage = response.GetPath("usageMetadata");
if (!usage.HasValue) if (!usage.HasValue)
{ {
return new ThorUsageResponse(); return null;
} }
var inputTokens = usage.Value.GetPath("promptTokenCount").GetInt(); var inputTokens = usage.Value.GetPath("promptTokenCount").GetInt();

View File

@@ -5,25 +5,69 @@ using System.Text.Json;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Yi.Framework.AiHub.Domain.AiGateWay.Exceptions; using Yi.Framework.AiHub.Domain.AiGateWay.Exceptions;
using Yi.Framework.AiHub.Domain.Shared.Dtos; using Yi.Framework.AiHub.Domain.Shared.Dtos;
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi.Responses; using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi.Responses;
namespace Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorGemini.Chats; namespace Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorGemini.Chats;
public class GeminiGenerateContentService(ILogger<GeminiGenerateContentService> logger,IHttpClientFactory httpClientFactory):IGeminiGenerateContentService public class GeminiGenerateContentService(
ILogger<GeminiGenerateContentService> logger,
IHttpClientFactory httpClientFactory) : IGeminiGenerateContentService
{ {
public IAsyncEnumerable<JsonElement?> GenerateContentStreamAsync(AiModelDescribe aiModelDescribe, JsonElement input, public async IAsyncEnumerable<JsonElement?> GenerateContentStreamAsync(AiModelDescribe options, JsonElement input,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
throw new NotImplementedException(); var response = await httpClientFactory.CreateClient().PostJsonAsync(
options?.Endpoint.TrimEnd('/') + $"/v1beta/models/{options.ModelId}:streamGenerateContent?alt=sse",
input, null, new Dictionary<string, string>()
{
{ "x-goog-api-key", options.ApiKey }
}).ConfigureAwait(false);
// 大于等于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 Exception("Gemini生成异常" + response.StatusCode);
}
using var stream = new StreamReader(await response.Content.ReadAsStreamAsync(cancellationToken));
using StreamReader reader = new(await response.Content.ReadAsStreamAsync(cancellationToken));
string? line = string.Empty;
while ((line = await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false)) != null)
{
line += Environment.NewLine;
if (string.IsNullOrWhiteSpace(line))
{
continue;
}
if (!line.StartsWith(OpenAIConstant.Data)) continue;
var data = line[OpenAIConstant.Data.Length..].Trim();
var result = JsonSerializer.Deserialize<JsonElement>(data,
ThorJsonSerializer.DefaultOptions);
yield return result;
}
} }
public async Task<JsonElement> GenerateContentAsync(AiModelDescribe options,JsonElement input, CancellationToken cancellationToken) public async Task<JsonElement> GenerateContentAsync(AiModelDescribe options, JsonElement input,
CancellationToken cancellationToken)
{ {
var response = await httpClientFactory.CreateClient().PostJsonAsync( var response = await httpClientFactory.CreateClient().PostJsonAsync(
options?.Endpoint.TrimEnd('/') + $"/v1beta/models/{options.ModelId}:generateContent", options?.Endpoint.TrimEnd('/') + $"/v1beta/models/{options.ModelId}:generateContent",
input,null, new Dictionary<string,string>() input, null, new Dictionary<string, string>()
{ {
{"x-goog-api-key",options.ApiKey} { "x-goog-api-key", options.ApiKey }
}).ConfigureAwait(false); }).ConfigureAwait(false);
if (response.StatusCode == HttpStatusCode.Unauthorized) if (response.StatusCode == HttpStatusCode.Unauthorized)
@@ -41,7 +85,8 @@ public class GeminiGenerateContentService(ILogger<GeminiGenerateContentService>
if (response.StatusCode >= HttpStatusCode.BadRequest) if (response.StatusCode >= HttpStatusCode.BadRequest)
{ {
var error = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); var error = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
logger.LogError("Gemini 生成异常 请求地址:{Address}, StatusCode: {StatusCode} Response: {Response}", options.Endpoint, logger.LogError("Gemini 生成异常 请求地址:{Address}, StatusCode: {StatusCode} Response: {Response}",
options.Endpoint,
response.StatusCode, error); response.StatusCode, error);
throw new BusinessException("Gemini 生成异常", response.StatusCode.ToString()); throw new BusinessException("Gemini 生成异常", response.StatusCode.ToString());

View File

@@ -799,7 +799,7 @@ public class AiGateWayManager : DomainService
/// <param name="sessionId"></param> /// <param name="sessionId"></param>
/// <param name="tokenId"></param> /// <param name="tokenId"></param>
/// <param name="cancellationToken"></param> /// <param name="cancellationToken"></param>
public async Task GeminiGenerateContentAsyncForStatisticsAsync(HttpContext httpContext, public async Task GeminiGenerateContentForStatisticsAsync(HttpContext httpContext,
string modelId, string modelId,
JsonElement request, JsonElement request,
Guid? userId = null, Guid? userId = null,
@@ -814,7 +814,7 @@ public class AiGateWayManager : DomainService
LazyServiceProvider.GetRequiredKeyedService<IGeminiGenerateContentService>(modelDescribe.HandlerName); LazyServiceProvider.GetRequiredKeyedService<IGeminiGenerateContentService>(modelDescribe.HandlerName);
var data = await chatService.GenerateContentAsync(modelDescribe, request, cancellationToken); var data = await chatService.GenerateContentAsync(modelDescribe, request, cancellationToken);
var tokenUsage= GeminiGenerateContentAcquirer.GetUsage(data); var tokenUsage = GeminiGenerateContentAcquirer.GetUsage(data);
tokenUsage.SetSupplementalMultiplier(modelDescribe.Multiplier); tokenUsage.SetSupplementalMultiplier(modelDescribe.Multiplier);
if (userId is not null) if (userId is not null)
@@ -847,9 +847,88 @@ public class AiGateWayManager : DomainService
await response.WriteAsJsonAsync(data, cancellationToken); await response.WriteAsJsonAsync(data, cancellationToken);
} }
/// <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">Token IdWeb端传null或Guid.Empty</param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task GeminiGenerateContentStreamForStatisticsAsync(
HttpContext httpContext,
string modelId,
JsonElement request,
Guid? userId = null,
Guid? sessionId = null,
Guid? tokenId = null,
CancellationToken cancellationToken = default)
{
var response = httpContext.Response;
// 设置响应头,声明是 SSE 流
response.ContentType = "text/event-stream;charset=utf-8;";
response.Headers.TryAdd("Cache-Control", "no-cache");
response.Headers.TryAdd("Connection", "keep-alive");
var modelDescribe = await GetModelAsync(ModelApiTypeEnum.GenerateContent, modelId);
var chatService =
LazyServiceProvider.GetRequiredKeyedService<IGeminiGenerateContentService>(modelDescribe.HandlerName);
var completeChatResponse = chatService.GenerateContentStreamAsync(modelDescribe,request, cancellationToken);
ThorUsageResponse? tokenUsage = null;
try
{
await foreach (var responseResult in completeChatResponse)
{
if ( responseResult!.Value.GetPath("candidates", 0, "finishReason").GetString() == "STOP")
{
tokenUsage = GeminiGenerateContentAcquirer.GetUsage(responseResult!.Value);
tokenUsage.SetSupplementalMultiplier(modelDescribe.Multiplier);
}
await response.WriteAsync($"data: {JsonSerializer.Serialize(responseResult)}\n\n", Encoding.UTF8, cancellationToken).ConfigureAwait(false);
await response.Body.FlushAsync(cancellationToken).ConfigureAwait(false);
}
}
catch (Exception e)
{
_logger.LogError(e, $"Ai生成异常");
var errorContent = $"生成Ai异常异常信息\n当前Ai模型{modelId}\n异常信息{e.Message}\n异常堆栈{e}";
throw new UserFriendlyException(errorContent);
}
await _aiMessageManager.CreateUserMessageAsync(userId, sessionId,
new MessageInputDto
{
Content = "不予存储" ,
ModelId = modelId,
TokenUsage = tokenUsage,
}, tokenId);
await _aiMessageManager.CreateSystemMessageAsync(userId, sessionId,
new MessageInputDto
{
Content = "不予存储" ,
ModelId = modelId,
TokenUsage = tokenUsage
}, tokenId);
await _usageStatisticsManager.SetUsageAsync(userId, modelId, tokenUsage, tokenId);
// 扣减尊享token包用量
if (userId.HasValue && tokenUsage is not null)
{
var totalTokens = tokenUsage.TotalTokens ?? 0;
if (tokenUsage.TotalTokens > 0)
{
await PremiumPackageManager.TryConsumeTokensAsync(userId.Value, totalTokens);
}
}
}