feat: 全面支持geminicli
This commit is contained in:
@@ -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,
|
||||||
@@ -322,6 +324,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"];
|
||||||
if (!string.IsNullOrWhiteSpace(authHeader) &&
|
if (!string.IsNullOrWhiteSpace(authHeader) &&
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -849,7 +849,86 @@ 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">Token Id(Web端传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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user