feat: 支持Claude模型API类型及尊享包校验与扣减逻辑

This commit is contained in:
ccnetcore
2025-10-14 22:17:21 +08:00
parent 31dc756868
commit 15713cf7fe
8 changed files with 963 additions and 30 deletions

View File

@@ -16,6 +16,7 @@ using Yi.Framework.AiHub.Domain.Entities;
using Yi.Framework.AiHub.Domain.Entities.Model;
using Yi.Framework.AiHub.Domain.Extensions;
using Yi.Framework.AiHub.Domain.Managers;
using Yi.Framework.AiHub.Domain.Shared.Consts;
using Yi.Framework.AiHub.Domain.Shared.Dtos;
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
using Yi.Framework.AiHub.Domain.Shared.Enums;
@@ -35,17 +36,19 @@ public class AiChatService : ApplicationService
private readonly AiBlacklistManager _aiBlacklistManager;
private readonly ILogger<AiChatService> _logger;
private readonly AiGateWayManager _aiGateWayManager;
private readonly PremiumPackageManager _premiumPackageManager;
public AiChatService(IHttpContextAccessor httpContextAccessor,
AiBlacklistManager aiBlacklistManager,
ISqlSugarRepository<AiModelEntity> aiModelRepository,
ILogger<AiChatService> logger, AiGateWayManager aiGateWayManager)
ILogger<AiChatService> logger, AiGateWayManager aiGateWayManager, PremiumPackageManager premiumPackageManager)
{
_httpContextAccessor = httpContextAccessor;
_aiBlacklistManager = aiBlacklistManager;
_aiModelRepository = aiModelRepository;
_logger = logger;
_aiGateWayManager = aiGateWayManager;
_premiumPackageManager = premiumPackageManager;
}
@@ -118,6 +121,17 @@ public class AiChatService : ApplicationService
}
}
//如果是尊享包服务,需要校验是是否尊享包足够
if (CurrentUser.IsAuthenticated && PremiumPackageConst.ModeIds.Contains(input.Model))
{
// 检查尊享token包用量
var availableTokens = await _premiumPackageManager.GetAvailableTokensAsync(CurrentUser.GetId());
if (availableTokens <= 0)
{
throw new UserFriendlyException("尊享token包用量不足请先购买尊享token包");
}
}
//ai网关代理httpcontext
await _aiGateWayManager.CompleteChatStreamForStatisticsAsync(_httpContextAccessor.HttpContext, input,
CurrentUser.Id, sessionId, cancellationToken);
@@ -154,7 +168,7 @@ public class AiChatService : ApplicationService
{
input.Model = "DeepSeek-R1-0528";
}
//ai网关代理httpcontext
await _aiGateWayManager.CompleteChatStreamForStatisticsAsync(_httpContextAccessor.HttpContext, input,
CurrentUser.Id, null, cancellationToken);

View File

@@ -57,6 +57,18 @@ public class OpenApiService : ApplicationService
var httpContext = this._httpContextAccessor.HttpContext;
var userId = await _tokenManager.GetUserIdAsync(GetTokenByHttpContext(httpContext));
await _aiBlacklistManager.VerifiyAiBlacklist(userId);
//如果是尊享包服务,需要校验是是否尊享包足够
if (PremiumPackageConst.ModeIds.Contains(input.Model))
{
// 检查尊享token包用量
var availableTokens = await _premiumPackageManager.GetAvailableTokensAsync(userId);
if (availableTokens <= 0)
{
throw new UserFriendlyException("尊享token包用量不足请先购买尊享token包");
}
}
//ai网关代理httpcontext
if (input.Stream == true)
{

View File

@@ -0,0 +1,8 @@
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
namespace Yi.Framework.AiHub.Domain.Shared.Consts;
public class PremiumPackageConst
{
public static List<string> ModeIds = ["claude-sonnet-4-5-20250929"];
}

View File

@@ -0,0 +1,7 @@
namespace Yi.Framework.AiHub.Domain.Shared.Enums;
public enum ModelApiTypeEnum
{
OpenAi,
Claude
}

View File

@@ -0,0 +1,861 @@
using System.Diagnostics;
using System.Net;
using System.Net.Http.Json;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using Yi.Framework.AiHub.Domain.Shared.Dtos;
using Yi.Framework.AiHub.Domain.Shared.Dtos.Anthropic;
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
namespace Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorClaude.Chats;
public sealed class ClaudiaChatCompletionsService(
IHttpClientFactory httpClientFactory,
ILogger<ClaudiaChatCompletionsService> logger)
: IChatCompletionService
{
public List<ThorChatChoiceResponse> CreateResponse(AnthropicChatCompletionDto completionDto)
{
var response = new ThorChatChoiceResponse();
var chatMessage = new ThorChatMessage();
if (completionDto == null)
{
return new List<ThorChatChoiceResponse>();
}
if (completionDto.content.Any(x => x.type.Equals("thinking", StringComparison.OrdinalIgnoreCase)))
{
// 将推理字段合并到返回对象去
chatMessage.ReasoningContent = completionDto.content
.First(x => x.type.Equals("thinking", StringComparison.OrdinalIgnoreCase)).Thinking;
chatMessage.Role = completionDto.role;
chatMessage.Content = completionDto.content
.First(x => x.type.Equals("text", StringComparison.OrdinalIgnoreCase)).text;
}
else
{
chatMessage.Role = completionDto.role;
chatMessage.Content = completionDto.content
.FirstOrDefault()?.text;
}
response.Delta = chatMessage;
response.Message = chatMessage;
if (completionDto.content.Any(x => x.type.Equals("tool_use", StringComparison.OrdinalIgnoreCase)))
{
var toolUse = completionDto.content
.First(x => x.type.Equals("tool_use", StringComparison.OrdinalIgnoreCase));
chatMessage.ToolCalls =
[
new()
{
Id = toolUse.id,
Function = new ThorChatMessageFunction()
{
Name = toolUse.name,
Arguments = JsonSerializer.Serialize(toolUse.input,
ThorJsonSerializer.DefaultOptions),
},
Index = 0,
}
];
return
[
response
];
}
return new List<ThorChatChoiceResponse> { response };
}
private object CreateMessage(List<ThorChatMessage> messages, AiModelDescribe options)
{
var list = new List<object>();
foreach (var message in messages)
{
// 如果是图片
if (message.ContentCalculated is IList<ThorChatMessageContent> contentCalculated)
{
list.Add(new
{
role = message.Role,
content = (List<object>)contentCalculated.Select<ThorChatMessageContent, object>(x =>
{
if (x.Type == "text")
{
if ("true".Equals(options.ModelExtraInfo, StringComparison.OrdinalIgnoreCase))
{
return new
{
type = "text",
text = x.Text,
cache_control = new
{
type = "ephemeral"
}
};
}
return new
{
type = "text",
text = x.Text
};
}
var isBase64 = x.ImageUrl?.Url.StartsWith("http") == true;
if ("true".Equals(options.ModelExtraInfo, StringComparison.OrdinalIgnoreCase))
{
return new
{
type = "image",
source = new
{
type = isBase64 ? "base64" : "url",
media_type = "image/png",
data = x.ImageUrl?.Url,
},
cache_control = new
{
type = "ephemeral"
}
};
}
return new
{
type = "image",
source = new
{
type = isBase64 ? "base64" : "url",
media_type = "image/png",
data = x.ImageUrl?.Url,
}
};
})
});
}
else
{
if ("true".Equals(options.ModelExtraInfo, StringComparison.OrdinalIgnoreCase))
{
if (message.Role == "system")
{
list.Add(new
{
type = "text",
text = message.Content,
cache_control = new
{
type = "ephemeral"
}
});
}
else
{
list.Add(new
{
role = message.Role,
content = message.Content
});
}
}
else
{
if (message.Role == "system")
{
list.Add(new
{
type = "text",
text = message.Content
});
}
else if (message.Role == "tool")
{
list.Add(new
{
role = "user",
content = new List<object>
{
new
{
type = "tool_result",
tool_use_id = message.ToolCallId,
content = message.Content
}
}
});
}
else if (message.Role == "assistant")
{
// {
// "role": "assistant",
// "content": [
// {
// "type": "text",
// "text": "<thinking>I need to use get_weather, and the user wants SF, which is likely San Francisco, CA.</thinking>"
// },
// {
// "type": "tool_use",
// "id": "toolu_01A09q90qw90lq917835lq9",
// "name": "get_weather",
// "input": {
// "location": "San Francisco, CA",
// "unit": "celsius"
// }
// }
// ]
// },
if (message.ToolCalls?.Count > 0)
{
var content = new List<object>();
if (!string.IsNullOrEmpty(message.Content))
{
content.Add(new
{
type = "text",
text = message.Content
});
}
foreach (var toolCall in message.ToolCalls)
{
content.Add(new
{
type = "tool_use",
id = toolCall.Id,
name = toolCall.Function?.Name,
input = JsonSerializer.Deserialize<Dictionary<string, object>>(
toolCall.Function?.Arguments, ThorJsonSerializer.DefaultOptions)
});
}
list.Add(new
{
role = "assistant",
content
});
}
else
{
list.Add(new
{
role = "assistant",
content = new List<object>
{
new
{
type = "text",
text = message.Content
}
}
});
}
}
else
{
list.Add(new
{
role = message.Role,
content = message.Content
});
}
}
}
}
return list;
}
public async IAsyncEnumerable<ThorChatCompletionsResponse> CompleteChatStreamAsync(AiModelDescribe options,
ThorChatCompletionsRequest input,
CancellationToken cancellationToken)
{
using var openai =
Activity.Current?.Source.StartActivity("Claudia 对话补全");
if (string.IsNullOrEmpty(options.Endpoint))
{
options.Endpoint = "https://api.anthropic.com/";
}
var client = httpClientFactory.CreateClient();
var headers = new Dictionary<string, string>
{
{ "x-api-key", options.ApiKey },
{ "anthropic-version", "2023-06-01" }
};
var isThinking = input.Model.EndsWith("thinking");
input.Model = input.Model.Replace("-thinking", string.Empty);
var budgetTokens = 1024;
if (input.MaxTokens is < 2048)
{
input.MaxTokens = 2048;
}
if (input.MaxTokens != null && input.MaxTokens / 2 < 1024)
{
budgetTokens = input.MaxTokens.Value / (4 * 3);
}
// budgetTokens最大4096
budgetTokens = Math.Min(budgetTokens, 4096);
object tool_choice;
if (input.ToolChoice is not null && input.ToolChoice.Type == "auto")
{
tool_choice = new
{
type = "auto",
disable_parallel_tool_use = false,
};
}
else if (input.ToolChoice is not null && input.ToolChoice.Type == "any")
{
tool_choice = new
{
type = "any",
disable_parallel_tool_use = false,
};
}
else if (input.ToolChoice is not null && input.ToolChoice.Type == "tool")
{
tool_choice = new
{
type = "tool",
name = input.ToolChoice.Function?.Name,
disable_parallel_tool_use = false,
};
}
else
{
tool_choice = null;
}
var response = await client.HttpRequestRaw(options.Endpoint.TrimEnd('/') + "/v1/messages", new
{
model = input.Model,
max_tokens = input.MaxTokens ?? 2048,
stream = true,
tool_choice,
system = CreateMessage(input.Messages.Where(x => x.Role == "system").ToList(), options),
messages = CreateMessage(input.Messages.Where(x => x.Role != "system").ToList(), options),
top_p = isThinking ? null : input.TopP,
thinking = isThinking
? new
{
type = "enabled",
budget_tokens = budgetTokens,
}
: null,
tools = input.Tools?.Select(x => new
{
name = x.Function?.Name,
description = x.Function?.Description,
input_schema = new
{
type = x.Function?.Parameters?.Type,
required = x.Function?.Parameters?.Required,
properties = x.Function?.Parameters?.Properties?.ToDictionary(y => y.Key, y => new
{
description = y.Value.Description,
type = y.Value.Type,
@enum = y.Value.Enum
})
}
}).ToArray(),
temperature = isThinking ? null : input.Temperature
}, string.Empty, headers);
openai?.SetTag("Model", input.Model);
openai?.SetTag("Response", response.StatusCode.ToString());
// 大于等于400的状态码都认为是异常
if (response.StatusCode >= HttpStatusCode.BadRequest)
{
var error = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
logger.LogError("OpenAI对话异常 请求地址:{Address}, StatusCode: {StatusCode} Response: {Response}",
options.Endpoint,
response.StatusCode, error);
throw new Exception("OpenAI对话异常" + 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;
var first = true;
var isThink = false;
string? toolId = null;
string? toolName = null;
while ((line = await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false)) != null)
{
line += Environment.NewLine;
if (line.StartsWith('{'))
{
logger.LogInformation("OpenAI对话异常 , StatusCode: {StatusCode} Response: {Response}", response.StatusCode,
line);
throw new Exception("OpenAI对话异常" + line);
}
if (line.StartsWith(OpenAIConstant.Data))
line = line[OpenAIConstant.Data.Length..];
line = line.Trim();
if (string.IsNullOrWhiteSpace(line)) continue;
if (line == OpenAIConstant.Done)
{
break;
}
if (line.StartsWith(':'))
{
continue;
}
if (line.StartsWith("event: ", StringComparison.OrdinalIgnoreCase))
{
continue;
}
var result = JsonSerializer.Deserialize<AnthropicStreamDto>(line,
ThorJsonSerializer.DefaultOptions);
if (result?.Type == "content_block_delta")
{
if (result.Delta.Type is "text" or "text_delta")
{
yield return new ThorChatCompletionsResponse()
{
Choices =
[
new()
{
Message = new ThorChatMessage()
{
Content = result.Delta.Text,
Role = "assistant",
}
}
],
Model = input.Model,
Id = result?.Message?.id,
Usage = new ThorUsageResponse()
{
CompletionTokens = result?.Message?.Usage?.OutputTokens,
PromptTokens = result?.Message?.Usage?.InputTokens,
}
};
}
else if (result.Delta.Type == "input_json_delta")
{
yield return new ThorChatCompletionsResponse()
{
Choices =
[
new ThorChatChoiceResponse()
{
Message = new ThorChatMessage()
{
ToolCalls =
[
new ThorToolCall()
{
Id = toolId,
Function = new ThorChatMessageFunction()
{
Name = toolName,
Arguments = result.Delta.PartialJson
}
}
],
Role = "tool",
}
}
],
Model = input.Model,
Usage = new ThorUsageResponse()
{
PromptTokens = result?.Message?.Usage?.InputTokens,
}
};
}
else
{
yield return new ThorChatCompletionsResponse()
{
Choices = new List<ThorChatChoiceResponse>()
{
new()
{
Message = new ThorChatMessage()
{
ReasoningContent = result.Delta.Thinking,
Role = "assistant",
}
}
},
Model = input.Model,
Id = result?.Message?.id,
Usage = new ThorUsageResponse()
{
CompletionTokens = result?.Message?.Usage?.OutputTokens,
PromptTokens = result?.Message?.Usage?.InputTokens
}
};
}
continue;
}
if (result?.Type == "content_block_start")
{
if (result?.ContentBlock?.Id is not null)
{
toolId = result.ContentBlock.Id;
}
if (result?.ContentBlock?.Name is not null)
{
toolName = result.ContentBlock.Name;
}
yield return new ThorChatCompletionsResponse()
{
Choices =
[
new ThorChatChoiceResponse()
{
Message = new ThorChatMessage()
{
ToolCalls =
[
new ThorToolCall()
{
Id = toolId,
Function = new ThorChatMessageFunction()
{
Name = toolName
}
}
],
Role = "tool",
}
}
],
Model = input.Model,
Usage = new ThorUsageResponse()
{
PromptTokens = result?.Message?.Usage?.InputTokens,
}
};
}
if (result.Type == "content_block_delta")
{
yield return new ThorChatCompletionsResponse()
{
Choices =
[
new ThorChatChoiceResponse()
{
Message = new ThorChatMessage()
{
ToolCallId = result?.ContentBlock?.Id,
FunctionCall = new ThorChatMessageFunction()
{
Name = result?.ContentBlock?.Name,
Arguments = result?.Delta?.PartialJson
},
Role = "tool",
}
}
],
Model = input.Model,
Usage = new ThorUsageResponse()
{
PromptTokens = result?.Message?.Usage?.InputTokens,
}
};
continue;
}
if (result.Type == "message_start")
{
yield return new ThorChatCompletionsResponse()
{
Choices =
[
new ThorChatChoiceResponse()
{
Message = new ThorChatMessage()
{
Content = result?.Delta?.Text,
Role = "assistant",
}
}
],
Model = input.Model,
Usage = new ThorUsageResponse()
{
PromptTokens = result?.Message?.Usage?.InputTokens,
}
};
continue;
}
if (result.Type == "message_delta")
{
yield return new ThorChatCompletionsResponse()
{
Choices =
[
new ThorChatChoiceResponse()
{
Message = new ThorChatMessage()
{
Content = result.Delta?.Text,
Role = "assistant",
}
}
],
Model = input.Model,
Usage = new ThorUsageResponse()
{
CompletionTokens = result.Usage?.OutputTokens,
InputTokens = result.Usage?.InputTokens
}
};
continue;
}
if (result.Message == null)
{
continue;
}
var chat = CreateResponse(result.Message);
var content = chat?.FirstOrDefault()?.Delta;
if (first && string.IsNullOrWhiteSpace(content?.Content) && string.IsNullOrEmpty(content?.ReasoningContent))
{
continue;
}
if (first && content.Content == OpenAIConstant.ThinkStart)
{
isThink = true;
continue;
// 需要将content的内容转换到其他字段
}
if (isThink && content.Content.Contains(OpenAIConstant.ThinkEnd))
{
isThink = false;
// 需要将content的内容转换到其他字段
continue;
}
if (isThink)
{
// 需要将content的内容转换到其他字段
foreach (var choice in chat)
{
choice.Delta.ReasoningContent = choice.Delta.Content;
choice.Delta.Content = string.Empty;
}
}
first = false;
yield return new ThorChatCompletionsResponse()
{
Choices = chat,
Model = input.Model,
Id = result.Message.id,
Usage = new ThorUsageResponse()
{
CompletionTokens = result.Message.Usage?.OutputTokens,
PromptTokens = result.Message.Usage?.InputTokens,
InputTokens = result.Message.Usage?.InputTokens,
OutputTokens = result.Message.Usage?.OutputTokens,
TotalTokens = result.Message.Usage?.InputTokens + result.Message.Usage?.OutputTokens
}
};
}
}
public async Task<ThorChatCompletionsResponse> CompleteChatAsync(AiModelDescribe options,
ThorChatCompletionsRequest input,
CancellationToken cancellationToken)
{
using var openai =
Activity.Current?.Source.StartActivity("Claudia 对话补全");
if (string.IsNullOrEmpty(options.Endpoint))
{
options.Endpoint = "https://api.anthropic.com/";
}
var client = httpClientFactory.CreateClient();
var headers = new Dictionary<string, string>
{
{ "x-api-key", options.ApiKey },
{ "anthropic-version", "2023-06-01" }
};
bool isThink = input.Model.EndsWith("-thinking");
input.Model = input.Model.Replace("-thinking", string.Empty);
var budgetTokens = 1024;
if (input.MaxTokens is < 2048)
{
input.MaxTokens = 2048;
}
if (input.MaxTokens != null && input.MaxTokens / 2 < 1024)
{
budgetTokens = input.MaxTokens.Value / (4 * 3);
}
object tool_choice;
if (input.ToolChoice is not null && input.ToolChoice.Type == "auto")
{
tool_choice = new
{
type = "auto",
disable_parallel_tool_use = false,
};
}
else if (input.ToolChoice is not null && input.ToolChoice.Type == "any")
{
tool_choice = new
{
type = "any",
disable_parallel_tool_use = false,
};
}
else if (input.ToolChoice is not null && input.ToolChoice.Type == "tool")
{
tool_choice = new
{
type = "tool",
name = input.ToolChoice.Function?.Name,
disable_parallel_tool_use = false,
};
}
else
{
tool_choice = null;
}
// budgetTokens最大4096
budgetTokens = Math.Min(budgetTokens, 4096);
var response = await client.PostJsonAsync(options.Endpoint.TrimEnd('/') + "/v1/messages", new
{
model = input.Model,
max_tokens = input.MaxTokens ?? 2000,
system = CreateMessage(input.Messages.Where(x => x.Role == "system").ToList(), options),
messages = CreateMessage(input.Messages.Where(x => x.Role != "system").ToList(), options),
top_p = isThink ? null : input.TopP,
tool_choice,
thinking = isThink
? new
{
type = "enabled",
budget_tokens = budgetTokens,
}
: null,
tools = input.Tools?.Select(x => new
{
name = x.Function?.Name,
description = x.Function?.Description,
input_schema = new
{
type = x.Function?.Parameters?.Type,
required = x.Function?.Parameters?.Required,
properties = x.Function?.Parameters?.Properties?.ToDictionary(y => y.Key, y => new
{
description = y.Value.Description,
type = y.Value.Type,
@enum = y.Value.Enum
})
}
}).ToArray(),
temperature = isThink ? null : input.Temperature
}, string.Empty, headers);
openai?.SetTag("Model", input.Model);
openai?.SetTag("Response", response.StatusCode.ToString());
// 大于等于400的状态码都认为是异常
if (response.StatusCode >= HttpStatusCode.BadRequest)
{
var error = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
logger.LogError("OpenAI对话异常 请求地址:{Address}, StatusCode: {StatusCode} Response: {Response}",
options.Endpoint,
response.StatusCode, error);
throw new Exception("OpenAI对话异常" + response.StatusCode.ToString());
}
var value =
await response.Content.ReadFromJsonAsync<AnthropicChatCompletionDto>(ThorJsonSerializer.DefaultOptions,
cancellationToken: cancellationToken);
var thor = new ThorChatCompletionsResponse()
{
Choices = CreateResponse(value),
Model = input.Model,
Id = value.id,
Usage = new ThorUsageResponse()
{
CompletionTokens = value.Usage.OutputTokens,
PromptTokens = value.Usage.InputTokens
}
};
if (value.Usage.CacheReadInputTokens != null)
{
thor.Usage.PromptTokensDetails ??= new ThorUsageResponsePromptTokensDetails()
{
CachedTokens = value.Usage.CacheReadInputTokens.Value,
};
if (value.Usage.InputTokens > 0)
{
thor.Usage.InputTokens = value.Usage.InputTokens;
}
if (value.Usage.OutputTokens > 0)
{
thor.Usage.CompletionTokens = value.Usage.OutputTokens;
thor.Usage.OutputTokens = value.Usage.OutputTokens;
}
}
thor.Usage.TotalTokens = thor.Usage.InputTokens + thor.Usage.OutputTokens;
return thor;
}
}

View File

@@ -52,7 +52,12 @@ public class AiModelEntity : Entity<Guid>, IOrderNum, ISoftDelete
public string? ExtraInfo { get; set; }
/// <summary>
/// 模型类型
/// 模型类型(聊天/图片等)
/// </summary>
public ModelTypeEnum ModelType { get; set; }
/// <summary>
/// 模型Api类型现支持同一个模型id多种接口格式
/// </summary>
public ModelApiTypeEnum ModelApiType { get; set; }
}

View File

@@ -12,11 +12,13 @@ using Volo.Abp.Domain.Services;
using Yi.Framework.AiHub.Domain.AiGateWay;
using Yi.Framework.AiHub.Domain.AiGateWay.Exceptions;
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.OpenAi;
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi.Embeddings;
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi.Images;
using Yi.Framework.AiHub.Domain.Shared.Enums;
using Yi.Framework.Core.Extensions;
using Yi.Framework.SqlSugarCore.Abstractions;
using JsonSerializer = System.Text.Json.JsonSerializer;
@@ -27,21 +29,24 @@ namespace Yi.Framework.AiHub.Domain.Managers;
public class AiGateWayManager : DomainService
{
private readonly ISqlSugarRepository<AiAppAggregateRoot> _aiAppRepository;
private readonly ISqlSugarRepository<AiModelEntity> _aiModelRepository;
private readonly ILogger<AiGateWayManager> _logger;
private readonly AiMessageManager _aiMessageManager;
private readonly UsageStatisticsManager _usageStatisticsManager;
private readonly ISpecialCompatible _specialCompatible;
private PremiumPackageManager? _premiumPackageManager;
public AiGateWayManager(ISqlSugarRepository<AiAppAggregateRoot> aiAppRepository, ILogger<AiGateWayManager> logger,
AiMessageManager aiMessageManager, UsageStatisticsManager usageStatisticsManager,
ISpecialCompatible specialCompatible)
ISpecialCompatible specialCompatible, ISqlSugarRepository<AiModelEntity> aiModelRepository)
{
_aiAppRepository = aiAppRepository;
_logger = logger;
_aiMessageManager = aiMessageManager;
_usageStatisticsManager = usageStatisticsManager;
_specialCompatible = specialCompatible;
_aiModelRepository = aiModelRepository;
}
private PremiumPackageManager PremiumPackageManager =>
@@ -50,17 +55,17 @@ public class AiGateWayManager : DomainService
/// <summary>
/// 获取模型
/// </summary>
/// <param name="modelApiType"></param>
/// <param name="modelId"></param>
/// <returns></returns>
private async Task<AiModelDescribe> GetModelAsync(string modelId)
private async Task<AiModelDescribe> GetModelAsync(ModelApiTypeEnum modelApiType, string modelId)
{
var allApp = await _aiAppRepository._DbQueryable.Includes(x => x.AiModels).ToListAsync();
foreach (var app in allApp)
{
var model = app.AiModels.FirstOrDefault(x => x.ModelId == modelId);
if (model is not null)
{
return new AiModelDescribe
var aiModelDescribe = await _aiModelRepository._DbQueryable
.LeftJoin<AiAppAggregateRoot>((model, app) => model.AiAppId == app.Id)
.Where((model, app) => model.ModelId == modelId)
.Where((model, app) => model.ModelApiType == modelApiType)
.Select((model, app) =>
new AiModelDescribe
{
AppId = app.Id,
AppName = app.Name,
@@ -73,11 +78,14 @@ public class AiGateWayManager : DomainService
Description = model.Description,
AppExtraUrl = app.ExtraUrl,
ModelExtraInfo = model.ExtraInfo
};
}
})
.FirstAsync();
if (aiModelDescribe is null)
{
throw new UserFriendlyException($"【{modelId}】模型当前版本【{modelApiType}】格式不支持");
}
throw new UserFriendlyException($"{modelId}模型当前版本不支持");
return aiModelDescribe;
}
@@ -92,7 +100,7 @@ public class AiGateWayManager : DomainService
[EnumeratorCancellation] CancellationToken cancellationToken)
{
_specialCompatible.Compatible(request);
var modelDescribe = await GetModelAsync(request.Model);
var modelDescribe = await GetModelAsync(ModelApiTypeEnum.OpenAi, request.Model);
var chatService =
LazyServiceProvider.GetRequiredKeyedService<IChatCompletionService>(modelDescribe.HandlerName);
@@ -122,7 +130,7 @@ public class AiGateWayManager : DomainService
var response = httpContext.Response;
// 设置响应头,声明是 json
//response.ContentType = "application/json; charset=UTF-8";
var modelDescribe = await GetModelAsync(request.Model);
var modelDescribe = await GetModelAsync(ModelApiTypeEnum.OpenAi, request.Model);
var chatService =
LazyServiceProvider.GetRequiredKeyedService<IChatCompletionService>(modelDescribe.HandlerName);
var data = await chatService.CompleteChatAsync(modelDescribe, request, cancellationToken);
@@ -277,6 +285,16 @@ public class AiGateWayManager : DomainService
});
await _usageStatisticsManager.SetUsageAsync(userId, request.Model, tokenUsage);
// 扣减尊享token包用量
if (userId is not null && PremiumPackageConst.ModeIds.Contains(request.Model))
{
var totalTokens = tokenUsage.TotalTokens ?? 0;
if (totalTokens > 0)
{
await PremiumPackageManager.ConsumeTokensAsync(userId.Value, totalTokens);
}
}
}
@@ -297,7 +315,7 @@ public class AiGateWayManager : DomainService
var model = request.Model;
if (string.IsNullOrEmpty(model)) model = "dall-e-2";
var modelDescribe = await GetModelAsync(model);
var modelDescribe = await GetModelAsync(ModelApiTypeEnum.OpenAi, model);
// 获取渠道指定的实现类型的服务
var imageService =
@@ -329,6 +347,16 @@ public class AiGateWayManager : DomainService
});
await _usageStatisticsManager.SetUsageAsync(userId, model, response.Usage);
// 扣减尊享token包用量
if (userId is not null && PremiumPackageConst.ModeIds.Contains(request.Model))
{
var totalTokens = response.Usage.TotalTokens ?? 0;
if (totalTokens > 0)
{
await PremiumPackageManager.ConsumeTokensAsync(userId.Value, totalTokens);
}
}
}
catch (Exception e)
{
@@ -357,7 +385,7 @@ public class AiGateWayManager : DomainService
using var embedding =
Activity.Current?.Source.StartActivity("向量模型调用");
var modelDescribe = await GetModelAsync(input.Model);
var modelDescribe = await GetModelAsync(ModelApiTypeEnum.OpenAi, input.Model);
// 获取渠道指定的实现类型的服务
var embeddingService =
@@ -461,7 +489,7 @@ public class AiGateWayManager : DomainService
[EnumeratorCancellation] CancellationToken cancellationToken)
{
_specialCompatible.AnthropicCompatible(request);
var modelDescribe = await GetModelAsync(request.Model);
var modelDescribe = await GetModelAsync(ModelApiTypeEnum.Claude, request.Model);
var chatService =
LazyServiceProvider.GetRequiredKeyedService<IAnthropicChatCompletionService>(modelDescribe.HandlerName);
@@ -491,7 +519,7 @@ public class AiGateWayManager : DomainService
var response = httpContext.Response;
// 设置响应头,声明是 json
//response.ContentType = "application/json; charset=UTF-8";
var modelDescribe = await GetModelAsync(request.Model);
var modelDescribe = await GetModelAsync(ModelApiTypeEnum.Claude, request.Model);
var chatService =
LazyServiceProvider.GetRequiredKeyedService<IAnthropicChatCompletionService>(modelDescribe.HandlerName);
var data = await chatService.ChatCompletionsAsync(modelDescribe, request, cancellationToken);
@@ -516,14 +544,10 @@ public class AiGateWayManager : DomainService
await _usageStatisticsManager.SetUsageAsync(userId.Value, request.Model, data.TokenUsage);
// 扣减尊享token包用量
var totalTokens = data.TokenUsage.TotalTokens??0;
var totalTokens = data.TokenUsage.TotalTokens ?? 0;
if (totalTokens > 0)
{
var consumeSuccess = await PremiumPackageManager.ConsumeTokensAsync(userId.Value, totalTokens);
if (!consumeSuccess)
{
_logger.LogWarning($"用户 {userId.Value} 尊享token包扣减失败消耗token数: {totalTokens}");
}
await PremiumPackageManager.ConsumeTokensAsync(userId.Value, totalTokens);
}
}
@@ -562,10 +586,11 @@ public class AiGateWayManager : DomainService
await foreach (var responseResult in completeChatResponse)
{
//message_start是为了保底机制
if (responseResult.Item1.Contains("message_delta")||responseResult.Item1.Contains("message_start"))
if (responseResult.Item1.Contains("message_delta") || responseResult.Item1.Contains("message_start"))
{
tokenUsage = responseResult.Item2?.TokenUsage;
}
backupSystemContent.Append(responseResult.Item2?.Delta?.Text);
await WriteAsEventStreamDataAsync(httpContext, responseResult.Item1, responseResult.Item2,
cancellationToken);
@@ -622,7 +647,7 @@ public class AiGateWayManager : DomainService
// 扣减尊享token包用量
if (userId.HasValue && tokenUsage is not null)
{
var totalTokens = tokenUsage.TotalTokens??0;
var totalTokens = tokenUsage.TotalTokens ?? 0;
if (totalTokens > 0)
{
var consumeSuccess = await PremiumPackageManager.ConsumeTokensAsync(userId.Value, totalTokens);

View File

@@ -42,7 +42,8 @@ namespace Yi.Framework.AiHub.Domain
nameof(DeepSeekChatCompletionsService));
services.AddKeyedTransient<IChatCompletionService, OpenAiChatCompletionsService>(
nameof(OpenAiChatCompletionsService));
services.AddKeyedTransient<IChatCompletionService, ClaudiaChatCompletionsService>(
nameof(ClaudiaChatCompletionsService));
#endregion
#region Anthropic ChatCompletion