feat: 新增claude接口转换支持
This commit is contained in:
@@ -5,6 +5,7 @@ using Volo.Abp.Application.Services;
|
|||||||
using Yi.Framework.AiHub.Domain.Entities.Model;
|
using Yi.Framework.AiHub.Domain.Entities.Model;
|
||||||
using Yi.Framework.AiHub.Domain.Extensions;
|
using Yi.Framework.AiHub.Domain.Extensions;
|
||||||
using Yi.Framework.AiHub.Domain.Managers;
|
using Yi.Framework.AiHub.Domain.Managers;
|
||||||
|
using Yi.Framework.AiHub.Domain.Shared.Dtos.Anthropic;
|
||||||
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
|
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.Embeddings;
|
||||||
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi.Images;
|
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi.Images;
|
||||||
@@ -117,6 +118,37 @@ public class OpenApiService : ApplicationService
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Anthropic对话
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="input"></param>
|
||||||
|
/// <param name="cancellationToken"></param>
|
||||||
|
[HttpPost("openApi/v1/messages")]
|
||||||
|
public async Task MessagesAsync([FromBody] AnthropicInput input,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
//前面都是校验,后面才是真正的调用
|
||||||
|
var httpContext = this._httpContextAccessor.HttpContext;
|
||||||
|
var userId = await _tokenManager.GetUserIdAsync(GetTokenByHttpContext(httpContext));
|
||||||
|
await _aiBlacklistManager.VerifiyAiBlacklist(userId);
|
||||||
|
//ai网关代理httpcontext
|
||||||
|
if (input.Stream)
|
||||||
|
{
|
||||||
|
await _aiGateWayManager.AnthropicCompleteChatStreamForStatisticsAsync(_httpContextAccessor.HttpContext, input,
|
||||||
|
userId, null, cancellationToken);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await _aiGateWayManager.AnthropicCompleteChatForStatisticsAsync(_httpContextAccessor.HttpContext, input, userId,
|
||||||
|
null,
|
||||||
|
cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#region 私有
|
||||||
|
|
||||||
private string? GetTokenByHttpContext(HttpContext httpContext)
|
private string? GetTokenByHttpContext(HttpContext httpContext)
|
||||||
{
|
{
|
||||||
// 获取Authorization头
|
// 获取Authorization头
|
||||||
@@ -138,4 +170,7 @@ public class OpenApiService : ApplicationService
|
|||||||
throw new UserFriendlyException("当前海外站点不支持大流量接口,请使用转发站点:https://ai.ccnetcore.com");
|
throw new UserFriendlyException("当前海外站点不支持大流量接口,请使用转发站点:https://ai.ccnetcore.com");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.Anthropic;
|
||||||
|
|
||||||
|
public sealed class AnthropicCacheControl
|
||||||
|
{
|
||||||
|
[JsonPropertyName("type")]
|
||||||
|
public string Type { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
|
||||||
|
|
||||||
|
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.Anthropic;
|
||||||
|
|
||||||
|
public class AnthropicStreamDto
|
||||||
|
{
|
||||||
|
[JsonPropertyName("type")] public string? Type { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("index")] public int? Index { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("content_block")] public AnthropicChatCompletionDtoContentBlock? ContentBlock { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("delta")] public AnthropicChatCompletionDtoDelta? Delta { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("message")] public AnthropicChatCompletionDto? Message { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("usage")] public AnthropicCompletionDtoUsage? Usage { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("error")] public AnthropicStreamErrorDto? Error { get; set; }
|
||||||
|
|
||||||
|
public ThorUsageResponse TokenUsage => new ThorUsageResponse
|
||||||
|
{
|
||||||
|
PromptTokens = Usage?.InputTokens,
|
||||||
|
InputTokens = Usage?.InputTokens,
|
||||||
|
OutputTokens = Usage?.OutputTokens,
|
||||||
|
InputTokensDetails = null,
|
||||||
|
CompletionTokens = Usage?.OutputTokens,
|
||||||
|
TotalTokens = Usage?.InputTokens + Usage?.OutputTokens,
|
||||||
|
PromptTokensDetails = null,
|
||||||
|
CompletionTokensDetails = null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AnthropicStreamErrorDto
|
||||||
|
{
|
||||||
|
[JsonPropertyName("type")] public string? Type { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("message")] public string? Message { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AnthropicChatCompletionDtoDelta
|
||||||
|
{
|
||||||
|
[JsonPropertyName("type")] public string? Type { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("text")] public string? Text { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("thinking")] public string? Thinking { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("partial_json")] public string? PartialJson { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("stop_reason")] public string? StopReason { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AnthropicChatCompletionDtoContentBlock
|
||||||
|
{
|
||||||
|
[JsonPropertyName("type")] public string? Type { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("thinking")] public string? Thinking { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("signature")] public string? Signature { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("id")] public string? Id { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("name")] public string? Name { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("input")] public object? Input { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("server_name")] public string? ServerName { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("is_error")] public bool? IsError { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("tool_use_id")] public string? ToolUseId { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("content")] public object? Content { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AnthropicChatCompletionDto
|
||||||
|
{
|
||||||
|
public string id { get; set; }
|
||||||
|
|
||||||
|
public string type { get; set; }
|
||||||
|
|
||||||
|
public string role { get; set; }
|
||||||
|
|
||||||
|
public AnthropicChatCompletionDtoContent[] content { get; set; }
|
||||||
|
|
||||||
|
public string model { get; set; }
|
||||||
|
|
||||||
|
public string stop_reason { get; set; }
|
||||||
|
|
||||||
|
public object stop_sequence { get; set; }
|
||||||
|
|
||||||
|
public AnthropicCompletionDtoUsage Usage { get; set; }
|
||||||
|
|
||||||
|
public ThorUsageResponse TokenUsage => new ThorUsageResponse
|
||||||
|
{
|
||||||
|
PromptTokens = Usage?.InputTokens,
|
||||||
|
InputTokens = Usage?.InputTokens,
|
||||||
|
OutputTokens = Usage?.OutputTokens,
|
||||||
|
InputTokensDetails = null,
|
||||||
|
CompletionTokens = Usage?.OutputTokens,
|
||||||
|
TotalTokens = Usage?.InputTokens + Usage?.OutputTokens,
|
||||||
|
PromptTokensDetails = null,
|
||||||
|
CompletionTokensDetails = null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AnthropicChatCompletionDtoContent
|
||||||
|
{
|
||||||
|
public string type { get; set; }
|
||||||
|
|
||||||
|
public string? text { get; set; }
|
||||||
|
|
||||||
|
public string? id { get; set; }
|
||||||
|
|
||||||
|
public string? name { get; set; }
|
||||||
|
|
||||||
|
public object? input { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("thinking")] public string? Thinking { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("partial_json")] public string? PartialJson { get; set; }
|
||||||
|
|
||||||
|
public string? signature { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AnthropicCompletionDtoUsage
|
||||||
|
{
|
||||||
|
[JsonPropertyName("input_tokens")] public int? InputTokens { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("cache_creation_input_tokens")]
|
||||||
|
public int? CacheCreationInputTokens { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("cache_read_input_tokens")]
|
||||||
|
public int? CacheReadInputTokens { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("output_tokens")] public int? OutputTokens { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("server_tool_use")] public AnthropicServerToolUse? ServerToolUse { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AnthropicServerToolUse
|
||||||
|
{
|
||||||
|
[JsonPropertyName("web_search_requests")]
|
||||||
|
public int? WebSearchRequests { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.Anthropic;
|
||||||
|
|
||||||
|
public sealed class AnthropicInput
|
||||||
|
{
|
||||||
|
[JsonPropertyName("stream")] public bool Stream { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("model")] public string Model { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("max_tokens")] public int? MaxTokens { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("messages")] public IList<AnthropicMessageInput> Messages { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("tools")] public IList<AnthropicMessageTool>? Tools { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("tool_choice")]
|
||||||
|
public object? ToolChoiceCalculated
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(ToolChoiceString))
|
||||||
|
{
|
||||||
|
return ToolChoiceString;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ToolChoice?.Type == "function")
|
||||||
|
{
|
||||||
|
return ToolChoice;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ToolChoice?.Type;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value is JsonElement jsonElement)
|
||||||
|
{
|
||||||
|
if (jsonElement.ValueKind == JsonValueKind.String)
|
||||||
|
{
|
||||||
|
ToolChoiceString = jsonElement.GetString();
|
||||||
|
}
|
||||||
|
else if (jsonElement.ValueKind == JsonValueKind.Object)
|
||||||
|
{
|
||||||
|
ToolChoice = jsonElement.Deserialize<AnthropicTooChoiceInput>(ThorJsonSerializer.DefaultOptions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ToolChoice = (AnthropicTooChoiceInput)value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[JsonIgnore] public string? ToolChoiceString { get; set; }
|
||||||
|
|
||||||
|
[JsonIgnore] public AnthropicTooChoiceInput? ToolChoice { get; set; }
|
||||||
|
|
||||||
|
[JsonIgnore] public IList<AnthropicMessageContent>? Systems { get; set; }
|
||||||
|
|
||||||
|
[JsonIgnore] public string? System { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("system")]
|
||||||
|
public object? SystemCalculated
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (System is not null && Systems is not null)
|
||||||
|
{
|
||||||
|
throw new ValidationException("System 和 Systems 字段不能同时有值");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (System is not null)
|
||||||
|
{
|
||||||
|
return System;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Systems!;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value is JsonElement str)
|
||||||
|
{
|
||||||
|
if (str.ValueKind == JsonValueKind.String)
|
||||||
|
{
|
||||||
|
System = value?.ToString();
|
||||||
|
}
|
||||||
|
else if (str.ValueKind == JsonValueKind.Array)
|
||||||
|
{
|
||||||
|
Systems = JsonSerializer.Deserialize<IList<AnthropicMessageContent>>(value?.ToString(),
|
||||||
|
ThorJsonSerializer.DefaultOptions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
System = value?.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[JsonPropertyName("thinking")] public AnthropicThinkingInput? Thinking { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("temperature")] public double? Temperature { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("metadata")] public Dictionary<string, object>? Metadata { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AnthropicThinkingInput
|
||||||
|
{
|
||||||
|
[JsonPropertyName("type")] public string Type { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("budget_tokens")] public int BudgetTokens { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AnthropicTooChoiceInput
|
||||||
|
{
|
||||||
|
[JsonPropertyName("type")] public string? Type { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("name")] public string? Name { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AnthropicMessageTool
|
||||||
|
{
|
||||||
|
[JsonPropertyName("name")] public string name { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("description")] public string? Description { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("input_schema")] public Input_schema InputSchema { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Input_schema
|
||||||
|
{
|
||||||
|
[JsonPropertyName("type")] public string Type { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("properties")] public Dictionary<string, InputSchemaValue>? Properties { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("required")] public string[]? Required { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class InputSchemaValue
|
||||||
|
{
|
||||||
|
public string type { get; set; }
|
||||||
|
|
||||||
|
public string description { get; set; }
|
||||||
|
|
||||||
|
public InputSchemaValueItems? items { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class InputSchemaValueItems
|
||||||
|
{
|
||||||
|
public string? type { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.Anthropic;
|
||||||
|
|
||||||
|
public class AnthropicMessageContent
|
||||||
|
{
|
||||||
|
[JsonPropertyName("cache_control")] public AnthropicCacheControl? CacheControl { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("type")] public string Type { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("text")] public string? Text { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("tool_use_id")] public string? ToolUseId { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("name")] public string? Name { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("id")] public string? Id { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("thinking")] public string? Thinking { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("input")] public object? Input { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("content")]
|
||||||
|
public object? Content
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_content is not null && _contents is not null)
|
||||||
|
{
|
||||||
|
throw new ValidationException("Messages 中 Content 和 Contents 字段不能同时有值");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_content is not null)
|
||||||
|
{
|
||||||
|
return _content;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _contents;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value is JsonElement str)
|
||||||
|
{
|
||||||
|
if (str.ValueKind == JsonValueKind.String)
|
||||||
|
{
|
||||||
|
_content = value?.ToString();
|
||||||
|
}
|
||||||
|
else if (str.ValueKind == JsonValueKind.Array)
|
||||||
|
{
|
||||||
|
_contents = JsonSerializer.Deserialize<List<AnthropicMessageContent>>(value?.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_content = value?.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string? _content;
|
||||||
|
|
||||||
|
private List<AnthropicMessageContent>? _contents;
|
||||||
|
|
||||||
|
public class AnthropicMessageContentSource
|
||||||
|
{
|
||||||
|
[JsonPropertyName("type")] public string Type { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("media_type")] public string? MediaType { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("data")] public string? Data { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[JsonPropertyName("source")] public AnthropicMessageContentSource? Source { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.Anthropic;
|
||||||
|
|
||||||
|
public class AnthropicMessageInput
|
||||||
|
{
|
||||||
|
[JsonPropertyName("role")]
|
||||||
|
public string Role { get; set; }
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public string? Content;
|
||||||
|
|
||||||
|
[JsonPropertyName("content")]
|
||||||
|
public object? ContentCalculated
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (Content is not null && Contents is not null)
|
||||||
|
{
|
||||||
|
throw new ValidationException("Messages 中 Content 和 Contents 字段不能同时有值");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Content is not null)
|
||||||
|
{
|
||||||
|
return Content;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Contents!;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value is JsonElement str)
|
||||||
|
{
|
||||||
|
if (str.ValueKind == JsonValueKind.String)
|
||||||
|
{
|
||||||
|
Content = value?.ToString();
|
||||||
|
}
|
||||||
|
else if (str.ValueKind == JsonValueKind.Array)
|
||||||
|
{
|
||||||
|
Contents = JsonSerializer.Deserialize<IList<AnthropicMessageContent>>(value?.ToString(),ThorJsonSerializer.DefaultOptions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Content = value?.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public IList<AnthropicMessageContent>? Contents;
|
||||||
|
}
|
||||||
@@ -0,0 +1,648 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
|
||||||
|
|
||||||
|
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.Anthropic;
|
||||||
|
|
||||||
|
public static class AnthropicToOpenAi
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 将AnthropicInput转换为ThorChatCompletionsRequest
|
||||||
|
/// </summary>
|
||||||
|
public static ThorChatCompletionsRequest ConvertAnthropicToOpenAi(AnthropicInput anthropicInput)
|
||||||
|
{
|
||||||
|
var openAiRequest = new ThorChatCompletionsRequest
|
||||||
|
{
|
||||||
|
Model = anthropicInput.Model,
|
||||||
|
MaxTokens = anthropicInput.MaxTokens,
|
||||||
|
Stream = anthropicInput.Stream,
|
||||||
|
Messages = new List<ThorChatMessage>(anthropicInput.Messages.Count)
|
||||||
|
};
|
||||||
|
|
||||||
|
// high medium minimal low
|
||||||
|
if (openAiRequest.Model.EndsWith("-high") ||
|
||||||
|
openAiRequest.Model.EndsWith("-medium") ||
|
||||||
|
openAiRequest.Model.EndsWith("-minimal") ||
|
||||||
|
openAiRequest.Model.EndsWith("-low"))
|
||||||
|
{
|
||||||
|
openAiRequest.ReasoningEffort = openAiRequest.Model switch
|
||||||
|
{
|
||||||
|
var model when model.EndsWith("-high") => "high",
|
||||||
|
var model when model.EndsWith("-medium") => "medium",
|
||||||
|
var model when model.EndsWith("-minimal") => "minimal",
|
||||||
|
var model when model.EndsWith("-low") => "low",
|
||||||
|
_ => "medium"
|
||||||
|
};
|
||||||
|
|
||||||
|
openAiRequest.Model = openAiRequest.Model.Replace("-high", "")
|
||||||
|
.Replace("-medium", "")
|
||||||
|
.Replace("-minimal", "")
|
||||||
|
.Replace("-low", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (anthropicInput.Thinking != null &&
|
||||||
|
anthropicInput.Thinking.Type.Equals("enabled", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
openAiRequest.Thinking = new ThorChatClaudeThinking()
|
||||||
|
{
|
||||||
|
BudgetToken = anthropicInput.Thinking.BudgetTokens,
|
||||||
|
Type = "enabled",
|
||||||
|
};
|
||||||
|
openAiRequest.EnableThinking = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (openAiRequest.Model.EndsWith("-thinking"))
|
||||||
|
{
|
||||||
|
openAiRequest.EnableThinking = true;
|
||||||
|
openAiRequest.Model = openAiRequest.Model.Replace("-thinking", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (openAiRequest.Stream == true)
|
||||||
|
{
|
||||||
|
openAiRequest.StreamOptions = new ThorStreamOptions()
|
||||||
|
{
|
||||||
|
IncludeUsage = true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(anthropicInput.System))
|
||||||
|
{
|
||||||
|
openAiRequest.Messages.Add(ThorChatMessage.CreateSystemMessage(anthropicInput.System));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (anthropicInput.Systems?.Count > 0)
|
||||||
|
{
|
||||||
|
foreach (var systemContent in anthropicInput.Systems)
|
||||||
|
{
|
||||||
|
openAiRequest.Messages.Add(ThorChatMessage.CreateSystemMessage(systemContent.Text ?? string.Empty));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理messages
|
||||||
|
if (anthropicInput.Messages != null)
|
||||||
|
{
|
||||||
|
foreach (var message in anthropicInput.Messages)
|
||||||
|
{
|
||||||
|
var thorMessages = ConvertAnthropicMessageToThor(message);
|
||||||
|
// 需要过滤 空消息
|
||||||
|
if (thorMessages.Count == 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
openAiRequest.Messages.AddRange(thorMessages);
|
||||||
|
}
|
||||||
|
|
||||||
|
openAiRequest.Messages = openAiRequest.Messages
|
||||||
|
.Where(m => !string.IsNullOrEmpty(m.Content) || m.Contents?.Count > 0 || m.ToolCalls?.Count > 0 ||
|
||||||
|
!string.IsNullOrEmpty(m.ToolCallId))
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理tools
|
||||||
|
if (anthropicInput.Tools is { Count: > 0 })
|
||||||
|
{
|
||||||
|
openAiRequest.Tools = anthropicInput.Tools.Where(x => x.name != "web_search")
|
||||||
|
.Select(ConvertAnthropicToolToThor).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断是否存在web_search
|
||||||
|
if (anthropicInput.Tools?.Any(x => x.name == "web_search") == true)
|
||||||
|
{
|
||||||
|
openAiRequest.WebSearchOptions = new ThorChatWebSearchOptions()
|
||||||
|
{
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理tool_choice
|
||||||
|
if (anthropicInput.ToolChoice != null)
|
||||||
|
{
|
||||||
|
openAiRequest.ToolChoice = ConvertAnthropicToolChoiceToThor(anthropicInput.ToolChoice);
|
||||||
|
}
|
||||||
|
|
||||||
|
return openAiRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 根据最后的内容块类型和OpenAI的完成原因确定Claude的停止原因
|
||||||
|
/// </summary>
|
||||||
|
public static string GetStopReasonByLastContentType(string? openAiFinishReason, string lastContentBlockType)
|
||||||
|
{
|
||||||
|
// 如果最后一个内容块是工具调用,优先返回tool_use
|
||||||
|
if (lastContentBlockType == "tool_use")
|
||||||
|
{
|
||||||
|
return "tool_use";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 否则使用标准的转换逻辑
|
||||||
|
return GetClaudeStopReason(openAiFinishReason);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建message_start事件
|
||||||
|
/// </summary>
|
||||||
|
public static AnthropicStreamDto CreateMessageStartEvent(string messageId, string model)
|
||||||
|
{
|
||||||
|
return new AnthropicStreamDto
|
||||||
|
{
|
||||||
|
Type = "message_start",
|
||||||
|
Message = new AnthropicChatCompletionDto
|
||||||
|
{
|
||||||
|
id = messageId,
|
||||||
|
type = "message",
|
||||||
|
role = "assistant",
|
||||||
|
model = model,
|
||||||
|
content = new AnthropicChatCompletionDtoContent[0],
|
||||||
|
Usage = new AnthropicCompletionDtoUsage
|
||||||
|
{
|
||||||
|
InputTokens = 0,
|
||||||
|
OutputTokens = 0,
|
||||||
|
CacheCreationInputTokens = 0,
|
||||||
|
CacheReadInputTokens = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建content_block_start事件
|
||||||
|
/// </summary>
|
||||||
|
public static AnthropicStreamDto CreateContentBlockStartEvent()
|
||||||
|
{
|
||||||
|
return new AnthropicStreamDto
|
||||||
|
{
|
||||||
|
Type = "content_block_start",
|
||||||
|
Index = 0,
|
||||||
|
ContentBlock = new AnthropicChatCompletionDtoContentBlock
|
||||||
|
{
|
||||||
|
Type = "text",
|
||||||
|
Id = null,
|
||||||
|
Name = null
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建thinking block start事件
|
||||||
|
/// </summary>
|
||||||
|
public static AnthropicStreamDto CreateThinkingBlockStartEvent()
|
||||||
|
{
|
||||||
|
return new AnthropicStreamDto
|
||||||
|
{
|
||||||
|
Type = "content_block_start",
|
||||||
|
Index = 0,
|
||||||
|
ContentBlock = new AnthropicChatCompletionDtoContentBlock
|
||||||
|
{
|
||||||
|
Type = "thinking",
|
||||||
|
Id = null,
|
||||||
|
Name = null
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建content_block_delta事件
|
||||||
|
/// </summary>
|
||||||
|
public static AnthropicStreamDto CreateContentBlockDeltaEvent(string text)
|
||||||
|
{
|
||||||
|
return new AnthropicStreamDto
|
||||||
|
{
|
||||||
|
Type = "content_block_delta",
|
||||||
|
Index = 0,
|
||||||
|
Delta = new AnthropicChatCompletionDtoDelta
|
||||||
|
{
|
||||||
|
Type = "text_delta",
|
||||||
|
Text = text
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建thinking delta事件
|
||||||
|
/// </summary>
|
||||||
|
public static AnthropicStreamDto CreateThinkingBlockDeltaEvent(string thinking)
|
||||||
|
{
|
||||||
|
return new AnthropicStreamDto
|
||||||
|
{
|
||||||
|
Type = "content_block_delta",
|
||||||
|
Index = 0,
|
||||||
|
Delta = new AnthropicChatCompletionDtoDelta
|
||||||
|
{
|
||||||
|
Type = "thinking",
|
||||||
|
Thinking = thinking
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建content_block_stop事件
|
||||||
|
/// </summary>
|
||||||
|
public static AnthropicStreamDto CreateContentBlockStopEvent()
|
||||||
|
{
|
||||||
|
return new AnthropicStreamDto
|
||||||
|
{
|
||||||
|
Type = "content_block_stop",
|
||||||
|
Index = 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建message_delta事件
|
||||||
|
/// </summary>
|
||||||
|
public static AnthropicStreamDto CreateMessageDeltaEvent(string finishReason, AnthropicCompletionDtoUsage usage)
|
||||||
|
{
|
||||||
|
return new AnthropicStreamDto
|
||||||
|
{
|
||||||
|
Type = "message_delta",
|
||||||
|
Usage = usage,
|
||||||
|
Delta = new AnthropicChatCompletionDtoDelta
|
||||||
|
{
|
||||||
|
StopReason = finishReason
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建message_stop事件
|
||||||
|
/// </summary>
|
||||||
|
public static AnthropicStreamDto CreateMessageStopEvent()
|
||||||
|
{
|
||||||
|
return new AnthropicStreamDto
|
||||||
|
{
|
||||||
|
Type = "message_stop"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建tool block start事件
|
||||||
|
/// </summary>
|
||||||
|
public static AnthropicStreamDto CreateToolBlockStartEvent(string? toolId, string? toolName)
|
||||||
|
{
|
||||||
|
return new AnthropicStreamDto
|
||||||
|
{
|
||||||
|
Type = "content_block_start",
|
||||||
|
Index = 0,
|
||||||
|
ContentBlock = new AnthropicChatCompletionDtoContentBlock
|
||||||
|
{
|
||||||
|
Type = "tool_use",
|
||||||
|
Id = toolId,
|
||||||
|
Name = toolName
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建tool delta事件
|
||||||
|
/// </summary>
|
||||||
|
public static AnthropicStreamDto CreateToolBlockDeltaEvent(string partialJson)
|
||||||
|
{
|
||||||
|
return new AnthropicStreamDto
|
||||||
|
{
|
||||||
|
Type = "content_block_delta",
|
||||||
|
Index = 0,
|
||||||
|
Delta = new AnthropicChatCompletionDtoDelta
|
||||||
|
{
|
||||||
|
Type = "input_json_delta",
|
||||||
|
PartialJson = partialJson
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 转换Anthropic消息为Thor消息列表
|
||||||
|
/// </summary>
|
||||||
|
public static List<ThorChatMessage> ConvertAnthropicMessageToThor(AnthropicMessageInput anthropicMessage)
|
||||||
|
{
|
||||||
|
var results = new List<ThorChatMessage>();
|
||||||
|
|
||||||
|
// 处理简单的字符串内容
|
||||||
|
if (anthropicMessage.Content != null)
|
||||||
|
{
|
||||||
|
var thorMessage = new ThorChatMessage
|
||||||
|
{
|
||||||
|
Role = anthropicMessage.Role,
|
||||||
|
Content = anthropicMessage.Content
|
||||||
|
};
|
||||||
|
results.Add(thorMessage);
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理多模态内容
|
||||||
|
if (anthropicMessage.Contents is { Count: > 0 })
|
||||||
|
{
|
||||||
|
var currentContents = new List<ThorChatMessageContent>();
|
||||||
|
var currentToolCalls = new List<ThorToolCall>();
|
||||||
|
|
||||||
|
foreach (var content in anthropicMessage.Contents)
|
||||||
|
{
|
||||||
|
switch (content.Type)
|
||||||
|
{
|
||||||
|
case "text":
|
||||||
|
currentContents.Add(ThorChatMessageContent.CreateTextContent(content.Text ?? string.Empty));
|
||||||
|
break;
|
||||||
|
case "thinking" when !string.IsNullOrEmpty(content.Thinking):
|
||||||
|
results.Add(new ThorChatMessage()
|
||||||
|
{
|
||||||
|
ReasoningContent = content.Thinking
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "image":
|
||||||
|
{
|
||||||
|
if (content.Source != null)
|
||||||
|
{
|
||||||
|
var imageUrl = content.Source.Type == "base64"
|
||||||
|
? $"data:{content.Source.MediaType};base64,{content.Source.Data}"
|
||||||
|
: content.Source.Data;
|
||||||
|
currentContents.Add(ThorChatMessageContent.CreateImageUrlContent(imageUrl ?? string.Empty));
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "tool_use":
|
||||||
|
{
|
||||||
|
// 如果有普通内容,先创建内容消息
|
||||||
|
if (currentContents.Count > 0)
|
||||||
|
{
|
||||||
|
if (currentContents.Count == 1 && currentContents.Any(x => x.Type == "text"))
|
||||||
|
{
|
||||||
|
var contentMessage = new ThorChatMessage
|
||||||
|
{
|
||||||
|
Role = anthropicMessage.Role,
|
||||||
|
ContentCalculated = currentContents.FirstOrDefault()?.Text ?? string.Empty
|
||||||
|
};
|
||||||
|
results.Add(contentMessage);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var contentMessage = new ThorChatMessage
|
||||||
|
{
|
||||||
|
Role = anthropicMessage.Role,
|
||||||
|
Contents = currentContents
|
||||||
|
};
|
||||||
|
results.Add(contentMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
currentContents = new List<ThorChatMessageContent>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 收集工具调用
|
||||||
|
var toolCall = new ThorToolCall
|
||||||
|
{
|
||||||
|
Id = content.Id,
|
||||||
|
Type = "function",
|
||||||
|
Function = new ThorChatMessageFunction
|
||||||
|
{
|
||||||
|
Name = content.Name,
|
||||||
|
Arguments = JsonSerializer.Serialize(content.Input)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
currentToolCalls.Add(toolCall);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "tool_result":
|
||||||
|
{
|
||||||
|
// 如果有普通内容,先创建内容消息
|
||||||
|
if (currentContents.Count > 0)
|
||||||
|
{
|
||||||
|
var contentMessage = new ThorChatMessage
|
||||||
|
{
|
||||||
|
Role = anthropicMessage.Role,
|
||||||
|
Contents = currentContents
|
||||||
|
};
|
||||||
|
results.Add(contentMessage);
|
||||||
|
currentContents = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果有工具调用,先创建工具调用消息
|
||||||
|
if (currentToolCalls.Count > 0)
|
||||||
|
{
|
||||||
|
var toolCallMessage = new ThorChatMessage
|
||||||
|
{
|
||||||
|
Role = anthropicMessage.Role,
|
||||||
|
ToolCalls = currentToolCalls
|
||||||
|
};
|
||||||
|
results.Add(toolCallMessage);
|
||||||
|
currentToolCalls = new List<ThorToolCall>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建工具结果消息
|
||||||
|
var toolMessage = new ThorChatMessage
|
||||||
|
{
|
||||||
|
Role = "tool",
|
||||||
|
ToolCallId = content.ToolUseId,
|
||||||
|
Content = content.Content?.ToString() ?? string.Empty
|
||||||
|
};
|
||||||
|
results.Add(toolMessage);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理剩余的内容
|
||||||
|
if (currentContents.Count > 0)
|
||||||
|
{
|
||||||
|
var contentMessage = new ThorChatMessage
|
||||||
|
{
|
||||||
|
Role = anthropicMessage.Role,
|
||||||
|
Contents = currentContents
|
||||||
|
};
|
||||||
|
results.Add(contentMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理剩余的工具调用
|
||||||
|
if (currentToolCalls.Count > 0)
|
||||||
|
{
|
||||||
|
var toolCallMessage = new ThorChatMessage
|
||||||
|
{
|
||||||
|
Role = anthropicMessage.Role,
|
||||||
|
ToolCalls = currentToolCalls
|
||||||
|
};
|
||||||
|
results.Add(toolCallMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有任何内容,返回一个空的消息
|
||||||
|
if (results.Count == 0)
|
||||||
|
{
|
||||||
|
results.Add(new ThorChatMessage
|
||||||
|
{
|
||||||
|
Role = anthropicMessage.Role,
|
||||||
|
Content = string.Empty
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果只有一个text则使用content字段
|
||||||
|
if (results is [{ Contents.Count: 1 }] &&
|
||||||
|
results.FirstOrDefault()?.Contents?.FirstOrDefault()?.Type == "text" &&
|
||||||
|
!string.IsNullOrEmpty(results.FirstOrDefault()?.Contents?.FirstOrDefault()?.Text))
|
||||||
|
{
|
||||||
|
return
|
||||||
|
[
|
||||||
|
new ThorChatMessage
|
||||||
|
{
|
||||||
|
Role = results[0].Role,
|
||||||
|
Content = results.FirstOrDefault()?.Contents?.FirstOrDefault()?.Text ?? string.Empty
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 转换Anthropic工具为Thor工具
|
||||||
|
/// </summary>
|
||||||
|
public static ThorToolDefinition ConvertAnthropicToolToThor(AnthropicMessageTool anthropicTool)
|
||||||
|
{
|
||||||
|
IDictionary<string, ThorToolFunctionPropertyDefinition> values =
|
||||||
|
new Dictionary<string, ThorToolFunctionPropertyDefinition>();
|
||||||
|
|
||||||
|
if (anthropicTool.InputSchema?.Properties != null)
|
||||||
|
{
|
||||||
|
foreach (var property in anthropicTool.InputSchema.Properties)
|
||||||
|
{
|
||||||
|
if (property.Value?.description != null)
|
||||||
|
{
|
||||||
|
var definitionType = new ThorToolFunctionPropertyDefinition()
|
||||||
|
{
|
||||||
|
Description = property.Value.description,
|
||||||
|
Type = property.Value.type
|
||||||
|
};
|
||||||
|
if (property.Value?.items?.type != null)
|
||||||
|
{
|
||||||
|
definitionType.Items = new ThorToolFunctionPropertyDefinition()
|
||||||
|
{
|
||||||
|
Type = property.Value.items.type
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
values.Add(property.Key, definitionType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return new ThorToolDefinition
|
||||||
|
{
|
||||||
|
Type = "function",
|
||||||
|
Function = new ThorToolFunctionDefinition
|
||||||
|
{
|
||||||
|
Name = anthropicTool.name,
|
||||||
|
Description = anthropicTool.Description,
|
||||||
|
Parameters = new ThorToolFunctionPropertyDefinition
|
||||||
|
{
|
||||||
|
Type = anthropicTool.InputSchema?.Type ?? "object",
|
||||||
|
Properties = values,
|
||||||
|
Required = anthropicTool.InputSchema?.Required
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 将OpenAI的完成原因转换为Claude的停止原因
|
||||||
|
/// </summary>
|
||||||
|
public static string GetClaudeStopReason(string? openAIFinishReason)
|
||||||
|
{
|
||||||
|
return openAIFinishReason switch
|
||||||
|
{
|
||||||
|
"stop" => "end_turn",
|
||||||
|
"length" => "max_tokens",
|
||||||
|
"tool_calls" => "tool_use",
|
||||||
|
"content_filter" => "stop_sequence",
|
||||||
|
_ => "end_turn"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 将OpenAI响应转换为Claude响应格式
|
||||||
|
/// </summary>
|
||||||
|
public static AnthropicChatCompletionDto ConvertOpenAIToClaude(ThorChatCompletionsResponse openAIResponse,
|
||||||
|
AnthropicInput originalRequest)
|
||||||
|
{
|
||||||
|
var claudeResponse = new AnthropicChatCompletionDto
|
||||||
|
{
|
||||||
|
id = openAIResponse.Id,
|
||||||
|
type = "message",
|
||||||
|
role = "assistant",
|
||||||
|
model = openAIResponse.Model ?? originalRequest.Model,
|
||||||
|
stop_reason = GetClaudeStopReason(openAIResponse.Choices?.FirstOrDefault()?.FinishReason),
|
||||||
|
stop_sequence = "",
|
||||||
|
content = []
|
||||||
|
};
|
||||||
|
|
||||||
|
if (openAIResponse.Choices is { Count: > 0 })
|
||||||
|
{
|
||||||
|
var choice = openAIResponse.Choices.First();
|
||||||
|
var contents = new List<AnthropicChatCompletionDtoContent>();
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(choice.Message.Content) && !string.IsNullOrEmpty(choice.Message.ReasoningContent))
|
||||||
|
{
|
||||||
|
contents.Add(new AnthropicChatCompletionDtoContent
|
||||||
|
{
|
||||||
|
type = "thinking",
|
||||||
|
Thinking = choice.Message.ReasoningContent
|
||||||
|
});
|
||||||
|
|
||||||
|
contents.Add(new AnthropicChatCompletionDtoContent
|
||||||
|
{
|
||||||
|
type = "text",
|
||||||
|
text = choice.Message.Content
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 处理思维内容
|
||||||
|
if (!string.IsNullOrEmpty(choice.Message.ReasoningContent))
|
||||||
|
contents.Add(new AnthropicChatCompletionDtoContent
|
||||||
|
{
|
||||||
|
type = "thinking",
|
||||||
|
Thinking = choice.Message.ReasoningContent
|
||||||
|
});
|
||||||
|
|
||||||
|
// 处理文本内容
|
||||||
|
if (!string.IsNullOrEmpty(choice.Message.Content))
|
||||||
|
contents.Add(new AnthropicChatCompletionDtoContent
|
||||||
|
{
|
||||||
|
type = "text",
|
||||||
|
text = choice.Message.Content
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理工具调用
|
||||||
|
if (choice.Message.ToolCalls is { Count: > 0 })
|
||||||
|
contents.AddRange(choice.Message.ToolCalls.Select(toolCall => new AnthropicChatCompletionDtoContent
|
||||||
|
{
|
||||||
|
type = "tool_use", id = toolCall.Id, name = toolCall.Function?.Name,
|
||||||
|
input = JsonSerializer.Deserialize<object>(toolCall.Function?.Arguments ?? "{}")
|
||||||
|
}));
|
||||||
|
|
||||||
|
claudeResponse.content = contents.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理使用情况统计 - 确保始终提供Usage信息
|
||||||
|
claudeResponse.Usage = new AnthropicCompletionDtoUsage
|
||||||
|
{
|
||||||
|
InputTokens = openAIResponse.Usage?.PromptTokens ?? 0,
|
||||||
|
OutputTokens = (int?)(openAIResponse.Usage?.CompletionTokens ?? 0),
|
||||||
|
CacheCreationInputTokens = openAIResponse.Usage?.PromptTokensDetails?.CachedTokens ?? 0,
|
||||||
|
CacheReadInputTokens = openAIResponse.Usage?.PromptTokensDetails?.CachedTokens ?? 0
|
||||||
|
};
|
||||||
|
|
||||||
|
return claudeResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 转换Anthropic工具选择为Thor工具选择
|
||||||
|
/// </summary>
|
||||||
|
public static ThorToolChoice ConvertAnthropicToolChoiceToThor(AnthropicTooChoiceInput anthropicToolChoice)
|
||||||
|
{
|
||||||
|
return new ThorToolChoice
|
||||||
|
{
|
||||||
|
Type = anthropicToolChoice.Type ?? "auto",
|
||||||
|
Function = anthropicToolChoice.Name != null
|
||||||
|
? new ThorToolChoiceFunctionTool { Name = anthropicToolChoice.Name }
|
||||||
|
: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.Anthropic;
|
||||||
|
|
||||||
|
public static class ThorJsonSerializer
|
||||||
|
{
|
||||||
|
public static JsonSerializerOptions DefaultOptions => new()
|
||||||
|
{
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||||
|
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||||
|
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -279,13 +279,10 @@ public class ThorChatCompletionsRequest
|
|||||||
|
|
||||||
[JsonPropertyName("thinking")] public ThorChatClaudeThinking? Thinking { get; set; }
|
[JsonPropertyName("thinking")] public ThorChatClaudeThinking? Thinking { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
[JsonPropertyName("enable_thinking")] public bool? EnableThinking { get; set; }
|
||||||
/// 参数验证
|
|
||||||
/// </summary>
|
[JsonPropertyName("web_search_options")]
|
||||||
/// <returns></returns>
|
public ThorChatWebSearchOptions? WebSearchOptions { get; set; } = null;
|
||||||
/// <exception cref="NotImplementedException"></exception>
|
|
||||||
public IEnumerable<ValidationResult> Validate()
|
[JsonPropertyName("reasoning_effort")] public string? ReasoningEffort { get; set; }
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
|
||||||
|
|
||||||
|
public class ThorChatWebSearchOptions
|
||||||
|
{
|
||||||
|
[JsonPropertyName("search_context_size")]
|
||||||
|
public string? SearchContextSize { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("user_location")]
|
||||||
|
public ThorUserLocation? UserLocation { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class ThorUserLocation
|
||||||
|
{
|
||||||
|
[JsonPropertyName("type")] public required string Type { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("approximate")]
|
||||||
|
public ThorUserLocationApproximate? Approximate { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class ThorUserLocationApproximate
|
||||||
|
{
|
||||||
|
[JsonPropertyName("city")]
|
||||||
|
public string? City { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("country")]
|
||||||
|
public string? Country { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("region")]
|
||||||
|
public string? Region { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("timezone")]
|
||||||
|
public string? Timezone { get; set; }
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Text.Json.Serialization;
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
|
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
|
||||||
|
|
||||||
@@ -25,56 +26,96 @@ public class ThorToolFunctionPropertyDefinition
|
|||||||
/// 表示字符串类型的函数对象
|
/// 表示字符串类型的函数对象
|
||||||
/// </summary>
|
/// </summary>
|
||||||
String,
|
String,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 表示整数类型的函数对象
|
/// 表示整数类型的函数对象
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Integer,
|
Integer,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 表示数字(包括浮点数等)类型的函数对象
|
/// 表示数字(包括浮点数等)类型的函数对象
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Number,
|
Number,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 表示对象类型的函数对象
|
/// 表示对象类型的函数对象
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Object,
|
Object,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 表示数组类型的函数对象
|
/// 表示数组类型的函数对象
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Array,
|
Array,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 表示布尔类型的函数对象
|
/// 表示布尔类型的函数对象
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Boolean,
|
Boolean,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 表示空值类型的函数对象
|
/// 表示空值类型的函数对象
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Null
|
Null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public string typeStr = "object";
|
||||||
|
|
||||||
|
public string[] Types;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 必填的。函数参数对象类型。默认值为“object”。
|
/// 必填的。函数参数对象类型。默认值为“object”。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonPropertyName("type")]
|
[JsonPropertyName("type")]
|
||||||
public object Type { get; set; } = "object";
|
public object Type
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (Types is { Length: > 0 })
|
||||||
|
{
|
||||||
|
return Types;
|
||||||
|
}
|
||||||
|
|
||||||
|
return typeStr;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value is JsonElement str)
|
||||||
|
{
|
||||||
|
switch (str.ValueKind)
|
||||||
|
{
|
||||||
|
case JsonValueKind.String:
|
||||||
|
typeStr = value?.ToString();
|
||||||
|
break;
|
||||||
|
case JsonValueKind.Array:
|
||||||
|
Types = JsonSerializer.Deserialize<string[]>(value?.ToString());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
typeStr = value?.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 可选。“函数参数”列表,作为从参数名称映射的字典
|
/// 可选。“函数参数”列表,作为从参数名称映射的字典
|
||||||
/// 对于描述类型的对象,可能还有可能的枚举值等等。
|
/// 对于描述类型的对象,可能还有可能的枚举值等等。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonPropertyName("properties")]
|
[JsonPropertyName("properties")]
|
||||||
public IDictionary<string, ThorToolFunctionPropertyDefinition?>? Properties { get; set; }
|
public IDictionary<string, ThorToolFunctionPropertyDefinition>? Properties { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 可选。列出必需的“function arguments”列表。
|
/// 可选。列出必需的“function arguments”列表。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonPropertyName("required")]
|
[JsonPropertyName("required")]
|
||||||
public List<string>? Required { get; set; }
|
public string[]? Required { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 可选。是否允许附加属性。默认值为true。
|
/// 可选。是否允许附加属性。默认值为true。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonPropertyName("additionalProperties")]
|
[JsonPropertyName("additionalProperties")]
|
||||||
public bool? AdditionalProperties { get; set; }
|
public object? AdditionalProperties { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 可选。参数描述。
|
/// 可选。参数描述。
|
||||||
@@ -219,11 +260,12 @@ public class ThorToolFunctionPropertyDefinition
|
|||||||
/// <param name="description"></param>
|
/// <param name="description"></param>
|
||||||
/// <param name="enum"></param>
|
/// <param name="enum"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public static ThorToolFunctionPropertyDefinition DefineObject(IDictionary<string, ThorToolFunctionPropertyDefinition>? properties,
|
public static ThorToolFunctionPropertyDefinition DefineObject(
|
||||||
List<string>? required,
|
IDictionary<string, ThorToolFunctionPropertyDefinition>? properties,
|
||||||
bool? additionalProperties,
|
string[]? required,
|
||||||
string? description,
|
object? additionalProperties,
|
||||||
List<string>? @enum)
|
string? description,
|
||||||
|
List<string>? @enum)
|
||||||
{
|
{
|
||||||
return new ThorToolFunctionPropertyDefinition
|
return new ThorToolFunctionPropertyDefinition
|
||||||
{
|
{
|
||||||
@@ -242,7 +284,6 @@ public class ThorToolFunctionPropertyDefinition
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="type">要转换的类型</param>
|
/// <param name="type">要转换的类型</param>
|
||||||
/// <returns>给定类型的字符串表示形式</returns>
|
/// <returns>给定类型的字符串表示形式</returns>
|
||||||
|
|
||||||
public static string ConvertTypeToString(FunctionObjectTypes type)
|
public static string ConvertTypeToString(FunctionObjectTypes type)
|
||||||
{
|
{
|
||||||
return type switch
|
return type switch
|
||||||
|
|||||||
@@ -4,5 +4,6 @@ public enum ModelTypeEnum
|
|||||||
{
|
{
|
||||||
Chat = 0,
|
Chat = 0,
|
||||||
Image = 1,
|
Image = 1,
|
||||||
Embedding = 2
|
Embedding = 2,
|
||||||
|
PremiumChat = 3
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
using Yi.Framework.AiHub.Domain.Shared.Dtos;
|
||||||
|
using Yi.Framework.AiHub.Domain.Shared.Dtos.Anthropic;
|
||||||
|
|
||||||
|
namespace Yi.Framework.AiHub.Domain.AiGateWay;
|
||||||
|
|
||||||
|
public interface IAnthropicChatCompletionService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 非流式对话补全
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">对话补全请求参数对象</param>
|
||||||
|
/// <param name="aiModelDescribe">平台参数对象</param>
|
||||||
|
/// <param name="cancellationToken">取消令牌</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task<AnthropicChatCompletionDto> ChatCompletionsAsync(AiModelDescribe aiModelDescribe,
|
||||||
|
AnthropicInput request,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 流式对话补全
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">对话补全请求参数对象</param>
|
||||||
|
/// <param name="aiModelDescribe">平台参数对象</param>
|
||||||
|
/// <param name="cancellationToken">取消令牌</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
IAsyncEnumerable<(string, AnthropicStreamDto?)> StreamChatCompletionsAsync(AiModelDescribe aiModelDescribe,
|
||||||
|
AnthropicInput request,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
|
using Yi.Framework.AiHub.Domain.Shared.Dtos.Anthropic;
|
||||||
|
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
|
||||||
|
|
||||||
namespace Yi.Framework.AiHub.Domain.AiGateWay;
|
namespace Yi.Framework.AiHub.Domain.AiGateWay;
|
||||||
|
|
||||||
public interface ISpecialCompatible
|
public interface ISpecialCompatible
|
||||||
{
|
{
|
||||||
public void Compatible(ThorChatCompletionsRequest request);
|
public void Compatible(ThorChatCompletionsRequest request);
|
||||||
|
public void AnthropicCompatible(AnthropicInput request);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
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 class AnthropicChatCompletionsService(IHttpClientFactory httpClientFactory,ILogger<AnthropicChatCompletionsService> logger)
|
||||||
|
: IAnthropicChatCompletionService
|
||||||
|
{
|
||||||
|
public async Task<AnthropicChatCompletionDto> ChatCompletionsAsync(AiModelDescribe options, AnthropicInput input,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
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 },
|
||||||
|
{ "authorization", "Bearer " + options.ApiKey },
|
||||||
|
{ "anthropic-version", "2023-06-01" }
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
bool isThink = input.Model.EndsWith("-thinking");
|
||||||
|
input.Model = input.Model.Replace("-thinking", string.Empty);
|
||||||
|
|
||||||
|
if (input.MaxTokens is < 2048)
|
||||||
|
{
|
||||||
|
input.MaxTokens = 2048;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isThink && input.Thinking is null)
|
||||||
|
{
|
||||||
|
input.Thinking = new AnthropicThinkingInput()
|
||||||
|
{
|
||||||
|
Type = "enabled",
|
||||||
|
BudgetTokens = 4000
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.Thinking is not null && input.Thinking.BudgetTokens > 0 && input.MaxTokens != null)
|
||||||
|
{
|
||||||
|
if (input.Thinking.BudgetTokens > input.MaxTokens)
|
||||||
|
{
|
||||||
|
input.Thinking.BudgetTokens = input.MaxTokens.Value - 1;
|
||||||
|
if (input.Thinking.BudgetTokens > 63999)
|
||||||
|
{
|
||||||
|
input.Thinking.BudgetTokens = 63999;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var response =
|
||||||
|
await client.PostJsonAsync(options.Endpoint.TrimEnd('/') + "/v1/messages", input, 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);
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async IAsyncEnumerable<(string, AnthropicStreamDto?)> StreamChatCompletionsAsync(AiModelDescribe options, AnthropicInput input,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
using var openai =
|
||||||
|
Activity.Current?.Source.StartActivity("Claudia 对话补全");
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(options.Endpoint))
|
||||||
|
{
|
||||||
|
options.Endpoint = "https://api.anthropic.com/";
|
||||||
|
}
|
||||||
|
|
||||||
|
var client = HttpClientFactory.GetHttpClient(options.Endpoint);
|
||||||
|
|
||||||
|
var headers = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ "x-api-key", options.ApiKey },
|
||||||
|
{ "authorization", options.ApiKey },
|
||||||
|
{ "anthropic-version", "2023-06-01" }
|
||||||
|
};
|
||||||
|
|
||||||
|
var isThinking = input.Model.EndsWith("thinking");
|
||||||
|
input.Model = input.Model.Replace("-thinking", string.Empty);
|
||||||
|
|
||||||
|
var response = await client.HttpRequestRaw(options.Endpoint.TrimEnd('/') + "/v1/messages", input, 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;
|
||||||
|
|
||||||
|
string? data = null;
|
||||||
|
string eventType = string.Empty;
|
||||||
|
|
||||||
|
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 (string.IsNullOrWhiteSpace(line))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.StartsWith("event:"))
|
||||||
|
{
|
||||||
|
eventType = line;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!line.StartsWith(OpenAIConstant.Data)) continue;
|
||||||
|
|
||||||
|
data = line[OpenAIConstant.Data.Length..].Trim();
|
||||||
|
|
||||||
|
var result = JsonSerializer.Deserialize<AnthropicStreamDto>(data,
|
||||||
|
ThorJsonSerializer.DefaultOptions);
|
||||||
|
|
||||||
|
yield return (eventType, result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,313 @@
|
|||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Volo.Abp.DependencyInjection;
|
||||||
|
using Yi.Framework.AiHub.Domain.Shared.Dtos;
|
||||||
|
using Yi.Framework.AiHub.Domain.Shared.Dtos.Anthropic;
|
||||||
|
|
||||||
|
namespace Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorCustomOpenAI.Chats;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// OpenAI到Claude适配器服务
|
||||||
|
/// 将Claude格式的请求转换为OpenAI格式,然后将OpenAI的响应转换为Claude格式
|
||||||
|
/// </summary>
|
||||||
|
public class CustomOpenAIAnthropicChatCompletionsService(
|
||||||
|
IAbpLazyServiceProvider serviceProvider,
|
||||||
|
ILogger<CustomOpenAIAnthropicChatCompletionsService> logger)
|
||||||
|
: IAnthropicChatCompletionService
|
||||||
|
{
|
||||||
|
private IChatCompletionService GetChatCompletionService()
|
||||||
|
{
|
||||||
|
return serviceProvider.GetRequiredKeyedService<IChatCompletionService>(nameof(OpenAiChatCompletionsService));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<AnthropicChatCompletionDto> ChatCompletionsAsync(AiModelDescribe aiModelDescribe,
|
||||||
|
AnthropicInput request,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
// 转换请求格式:Claude -> OpenAI
|
||||||
|
var openAIRequest = AnthropicToOpenAi.ConvertAnthropicToOpenAi(request);
|
||||||
|
|
||||||
|
if (openAIRequest.Model.StartsWith("gpt-5"))
|
||||||
|
{
|
||||||
|
openAIRequest.MaxCompletionTokens = request.MaxTokens;
|
||||||
|
openAIRequest.MaxTokens = null;
|
||||||
|
}
|
||||||
|
else if (openAIRequest.Model.StartsWith("o3-mini") || openAIRequest.Model.StartsWith("o4-mini"))
|
||||||
|
{
|
||||||
|
openAIRequest.MaxCompletionTokens = request.MaxTokens;
|
||||||
|
openAIRequest.MaxTokens = null;
|
||||||
|
openAIRequest.Temperature = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用OpenAI服务
|
||||||
|
var openAIResponse =
|
||||||
|
await GetChatCompletionService().CompleteChatAsync(aiModelDescribe,openAIRequest, cancellationToken);
|
||||||
|
|
||||||
|
// 转换响应格式:OpenAI -> Claude
|
||||||
|
var claudeResponse = AnthropicToOpenAi.ConvertOpenAIToClaude(openAIResponse, request);
|
||||||
|
|
||||||
|
return claudeResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async IAsyncEnumerable<(string, AnthropicStreamDto?)> StreamChatCompletionsAsync(AiModelDescribe aiModelDescribe,
|
||||||
|
AnthropicInput request,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var openAIRequest = AnthropicToOpenAi.ConvertAnthropicToOpenAi(request);
|
||||||
|
openAIRequest.Stream = true;
|
||||||
|
|
||||||
|
if (openAIRequest.Model.StartsWith("gpt-5"))
|
||||||
|
{
|
||||||
|
openAIRequest.MaxCompletionTokens = request.MaxTokens;
|
||||||
|
openAIRequest.MaxTokens = null;
|
||||||
|
}
|
||||||
|
else if (openAIRequest.Model.StartsWith("o3-mini") || openAIRequest.Model.StartsWith("o4-mini"))
|
||||||
|
{
|
||||||
|
openAIRequest.MaxCompletionTokens = request.MaxTokens;
|
||||||
|
openAIRequest.MaxTokens = null;
|
||||||
|
openAIRequest.Temperature = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var messageId = Guid.NewGuid().ToString();
|
||||||
|
var hasStarted = false;
|
||||||
|
var hasTextContentBlockStarted = false;
|
||||||
|
var hasThinkingContentBlockStarted = false;
|
||||||
|
var toolBlocksStarted = new Dictionary<int, bool>(); // 使用索引而不是ID
|
||||||
|
var toolCallIds = new Dictionary<int, string>(); // 存储每个索引对应的ID
|
||||||
|
var toolCallIndexToBlockIndex = new Dictionary<int, int>(); // 工具调用索引到块索引的映射
|
||||||
|
var accumulatedUsage = new AnthropicCompletionDtoUsage();
|
||||||
|
var isFinished = false;
|
||||||
|
var currentContentBlockType = ""; // 跟踪当前内容块类型
|
||||||
|
var currentBlockIndex = 0; // 跟踪当前块索引
|
||||||
|
var lastContentBlockType = ""; // 跟踪最后一个内容块类型,用于确定停止原因
|
||||||
|
|
||||||
|
await foreach (var openAIResponse in GetChatCompletionService().CompleteChatStreamAsync(aiModelDescribe,openAIRequest,
|
||||||
|
cancellationToken))
|
||||||
|
{
|
||||||
|
// 发送message_start事件
|
||||||
|
if (!hasStarted && openAIResponse.Choices?.Count > 0 &&
|
||||||
|
openAIResponse.Choices.Any(x => x.Delta.ToolCalls?.Count > 0) == false)
|
||||||
|
{
|
||||||
|
hasStarted = true;
|
||||||
|
var messageStartEvent = AnthropicToOpenAi.CreateMessageStartEvent(messageId, request.Model);
|
||||||
|
yield return ("message_start", messageStartEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新使用情况统计
|
||||||
|
if (openAIResponse.Usage != null)
|
||||||
|
{
|
||||||
|
// 使用最新的token计数(OpenAI通常在最后的响应中提供完整的统计)
|
||||||
|
if (openAIResponse.Usage.PromptTokens.HasValue)
|
||||||
|
{
|
||||||
|
accumulatedUsage.InputTokens = openAIResponse.Usage.PromptTokens.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (openAIResponse.Usage.CompletionTokens.HasValue)
|
||||||
|
{
|
||||||
|
accumulatedUsage.OutputTokens = (int)openAIResponse.Usage.CompletionTokens.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (openAIResponse.Usage.PromptTokensDetails?.CachedTokens.HasValue == true)
|
||||||
|
{
|
||||||
|
accumulatedUsage.CacheReadInputTokens =
|
||||||
|
openAIResponse.Usage.PromptTokensDetails.CachedTokens.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录调试信息
|
||||||
|
logger.LogDebug("OpenAI Usage更新: Input={InputTokens}, Output={OutputTokens}, CacheRead={CacheRead}",
|
||||||
|
accumulatedUsage.InputTokens, accumulatedUsage.OutputTokens,
|
||||||
|
accumulatedUsage.CacheReadInputTokens);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (openAIResponse.Choices is { Count: > 0 })
|
||||||
|
{
|
||||||
|
var choice = openAIResponse.Choices.First();
|
||||||
|
|
||||||
|
// 处理内容
|
||||||
|
if (!string.IsNullOrEmpty(choice.Delta?.Content))
|
||||||
|
{
|
||||||
|
// 如果当前有其他类型的内容块在运行,先结束它们
|
||||||
|
if (currentContentBlockType != "text" && !string.IsNullOrEmpty(currentContentBlockType))
|
||||||
|
{
|
||||||
|
var stopEvent = AnthropicToOpenAi.CreateContentBlockStopEvent();
|
||||||
|
stopEvent.Index = currentBlockIndex;
|
||||||
|
yield return ("content_block_stop", stopEvent);
|
||||||
|
currentBlockIndex++; // 切换内容块时增加索引
|
||||||
|
currentContentBlockType = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送content_block_start事件(仅第一次)
|
||||||
|
if (!hasTextContentBlockStarted || currentContentBlockType != "text")
|
||||||
|
{
|
||||||
|
hasTextContentBlockStarted = true;
|
||||||
|
currentContentBlockType = "text";
|
||||||
|
lastContentBlockType = "text";
|
||||||
|
var contentBlockStartEvent = AnthropicToOpenAi.CreateContentBlockStartEvent();
|
||||||
|
contentBlockStartEvent.Index = currentBlockIndex;
|
||||||
|
yield return ("content_block_start",
|
||||||
|
contentBlockStartEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送content_block_delta事件
|
||||||
|
var contentDeltaEvent = AnthropicToOpenAi.CreateContentBlockDeltaEvent(choice.Delta.Content);
|
||||||
|
contentDeltaEvent.Index = currentBlockIndex;
|
||||||
|
yield return ("content_block_delta",
|
||||||
|
contentDeltaEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理工具调用
|
||||||
|
if (choice.Delta?.ToolCalls is { Count: > 0 })
|
||||||
|
{
|
||||||
|
foreach (var toolCall in choice.Delta.ToolCalls)
|
||||||
|
{
|
||||||
|
var toolCallIndex = toolCall.Index; // 使用索引来标识工具调用
|
||||||
|
|
||||||
|
// 发送tool_use content_block_start事件
|
||||||
|
if (toolBlocksStarted.TryAdd(toolCallIndex, true))
|
||||||
|
{
|
||||||
|
// 如果当前有文本或thinking内容块在运行,先结束它们
|
||||||
|
if (currentContentBlockType == "text" || currentContentBlockType == "thinking")
|
||||||
|
{
|
||||||
|
var stopEvent = AnthropicToOpenAi.CreateContentBlockStopEvent();
|
||||||
|
stopEvent.Index = currentBlockIndex;
|
||||||
|
yield return ("content_block_stop", stopEvent);
|
||||||
|
currentBlockIndex++; // 增加块索引
|
||||||
|
}
|
||||||
|
// 如果当前有其他工具调用在运行,也需要结束它们
|
||||||
|
else if (currentContentBlockType == "tool_use")
|
||||||
|
{
|
||||||
|
var stopEvent = AnthropicToOpenAi.CreateContentBlockStopEvent();
|
||||||
|
stopEvent.Index = currentBlockIndex;
|
||||||
|
yield return ("content_block_stop", stopEvent);
|
||||||
|
currentBlockIndex++; // 增加块索引
|
||||||
|
}
|
||||||
|
|
||||||
|
currentContentBlockType = "tool_use";
|
||||||
|
lastContentBlockType = "tool_use";
|
||||||
|
|
||||||
|
// 为此工具调用分配一个新的块索引
|
||||||
|
toolCallIndexToBlockIndex[toolCallIndex] = currentBlockIndex;
|
||||||
|
|
||||||
|
// 保存工具调用的ID(如果有的话)
|
||||||
|
if (!string.IsNullOrEmpty(toolCall.Id))
|
||||||
|
{
|
||||||
|
toolCallIds[toolCallIndex] = toolCall.Id;
|
||||||
|
}
|
||||||
|
else if (!toolCallIds.ContainsKey(toolCallIndex))
|
||||||
|
{
|
||||||
|
// 如果没有ID且之前也没有保存过,生成一个新的ID
|
||||||
|
toolCallIds[toolCallIndex] = Guid.NewGuid().ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
var toolBlockStartEvent = AnthropicToOpenAi.CreateToolBlockStartEvent(
|
||||||
|
toolCallIds[toolCallIndex],
|
||||||
|
toolCall.Function?.Name);
|
||||||
|
toolBlockStartEvent.Index = currentBlockIndex;
|
||||||
|
yield return ("content_block_start",
|
||||||
|
toolBlockStartEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果有增量的参数,发送content_block_delta事件
|
||||||
|
if (!string.IsNullOrEmpty(toolCall.Function?.Arguments))
|
||||||
|
{
|
||||||
|
var toolDeltaEvent =
|
||||||
|
AnthropicToOpenAi.CreateToolBlockDeltaEvent(toolCall.Function.Arguments);
|
||||||
|
// 使用该工具调用对应的块索引
|
||||||
|
toolDeltaEvent.Index = toolCallIndexToBlockIndex[toolCallIndex];
|
||||||
|
yield return ("content_block_delta",
|
||||||
|
toolDeltaEvent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理推理内容
|
||||||
|
if (!string.IsNullOrEmpty(choice.Delta?.ReasoningContent))
|
||||||
|
{
|
||||||
|
// 如果当前有其他类型的内容块在运行,先结束它们
|
||||||
|
if (currentContentBlockType != "thinking" && !string.IsNullOrEmpty(currentContentBlockType))
|
||||||
|
{
|
||||||
|
var stopEvent = AnthropicToOpenAi.CreateContentBlockStopEvent();
|
||||||
|
stopEvent.Index = currentBlockIndex;
|
||||||
|
yield return ("content_block_stop", stopEvent);
|
||||||
|
currentBlockIndex++; // 增加块索引
|
||||||
|
currentContentBlockType = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对于推理内容,也需要发送对应的事件
|
||||||
|
if (!hasThinkingContentBlockStarted || currentContentBlockType != "thinking")
|
||||||
|
{
|
||||||
|
hasThinkingContentBlockStarted = true;
|
||||||
|
currentContentBlockType = "thinking";
|
||||||
|
lastContentBlockType = "thinking";
|
||||||
|
var thinkingBlockStartEvent = AnthropicToOpenAi.CreateThinkingBlockStartEvent();
|
||||||
|
thinkingBlockStartEvent.Index = currentBlockIndex;
|
||||||
|
yield return ("content_block_start",
|
||||||
|
thinkingBlockStartEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
var thinkingDeltaEvent =
|
||||||
|
AnthropicToOpenAi.CreateThinkingBlockDeltaEvent(choice.Delta.ReasoningContent);
|
||||||
|
thinkingDeltaEvent.Index = currentBlockIndex;
|
||||||
|
yield return ("content_block_delta",
|
||||||
|
thinkingDeltaEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理结束
|
||||||
|
if (!string.IsNullOrEmpty(choice.FinishReason) && !isFinished)
|
||||||
|
{
|
||||||
|
isFinished = true;
|
||||||
|
|
||||||
|
// 发送content_block_stop事件(如果有活跃的内容块)
|
||||||
|
if (!string.IsNullOrEmpty(currentContentBlockType))
|
||||||
|
{
|
||||||
|
var contentBlockStopEvent = AnthropicToOpenAi.CreateContentBlockStopEvent();
|
||||||
|
contentBlockStopEvent.Index = currentBlockIndex;
|
||||||
|
yield return ("content_block_stop",
|
||||||
|
contentBlockStopEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送message_delta事件
|
||||||
|
var messageDeltaEvent = AnthropicToOpenAi.CreateMessageDeltaEvent(
|
||||||
|
AnthropicToOpenAi.GetStopReasonByLastContentType(choice.FinishReason, lastContentBlockType),
|
||||||
|
accumulatedUsage);
|
||||||
|
|
||||||
|
// 记录最终Usage统计
|
||||||
|
logger.LogDebug(
|
||||||
|
"流式响应结束,最终Usage: Input={InputTokens}, Output={OutputTokens}, CacheRead={CacheRead}",
|
||||||
|
accumulatedUsage.InputTokens, accumulatedUsage.OutputTokens,
|
||||||
|
accumulatedUsage.CacheReadInputTokens);
|
||||||
|
|
||||||
|
yield return ("message_delta",
|
||||||
|
messageDeltaEvent);
|
||||||
|
|
||||||
|
// 发送message_stop事件
|
||||||
|
var messageStopEvent = AnthropicToOpenAi.CreateMessageStopEvent();
|
||||||
|
yield return ("message_stop",
|
||||||
|
messageStopEvent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保流正确结束
|
||||||
|
if (!isFinished)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(currentContentBlockType))
|
||||||
|
{
|
||||||
|
var contentBlockStopEvent = AnthropicToOpenAi.CreateContentBlockStopEvent();
|
||||||
|
contentBlockStopEvent.Index = currentBlockIndex;
|
||||||
|
yield return ("content_block_stop",
|
||||||
|
contentBlockStopEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
var messageDeltaEvent =
|
||||||
|
AnthropicToOpenAi.CreateMessageDeltaEvent(
|
||||||
|
AnthropicToOpenAi.GetStopReasonByLastContentType("end_turn", lastContentBlockType),
|
||||||
|
accumulatedUsage);
|
||||||
|
yield return ("message_delta", messageDeltaEvent);
|
||||||
|
|
||||||
|
var messageStopEvent = AnthropicToOpenAi.CreateMessageStopEvent();
|
||||||
|
yield return ("message_stop",
|
||||||
|
messageStopEvent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
namespace Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorCustomOpenAI.Chats;
|
||||||
|
|
||||||
|
public sealed class OpenAiChatCompletionsService(ILogger<OpenAiChatCompletionsService> logger,IHttpClientFactory httpClientFactory)
|
||||||
|
: IChatCompletionService
|
||||||
|
{
|
||||||
|
public async IAsyncEnumerable<ThorChatCompletionsResponse> CompleteChatStreamAsync(AiModelDescribe options,
|
||||||
|
ThorChatCompletionsRequest chatCompletionCreate,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
using var openai =
|
||||||
|
Activity.Current?.Source.StartActivity("OpenAI 对话流式补全");
|
||||||
|
|
||||||
|
var response = await httpClientFactory.CreateClient().HttpRequestRaw(
|
||||||
|
options?.Endpoint.TrimEnd('/') + "/chat/completions",
|
||||||
|
chatCompletionCreate, options.ApiKey);
|
||||||
|
|
||||||
|
openai?.SetTag("Model", chatCompletionCreate.Model);
|
||||||
|
openai?.SetTag("Response", response.StatusCode.ToString());
|
||||||
|
|
||||||
|
if (response.StatusCode == HttpStatusCode.Unauthorized)
|
||||||
|
{
|
||||||
|
throw new UnauthorizedAccessException();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果限流则抛出限流异常
|
||||||
|
if (response.StatusCode == HttpStatusCode.TooManyRequests)
|
||||||
|
{
|
||||||
|
throw new ThorRateLimitException();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 大于等于400的状态码都认为是异常
|
||||||
|
if (response.StatusCode >= HttpStatusCode.BadRequest)
|
||||||
|
{
|
||||||
|
var error = await response.Content.ReadAsStringAsync();
|
||||||
|
logger.LogError("OpenAI对话异常 , StatusCode: {StatusCode} 错误响应内容:{Content}", response.StatusCode,
|
||||||
|
error);
|
||||||
|
|
||||||
|
throw new BusinessException("OpenAI对话异常:" + error, response.StatusCode.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
while ((line = await reader.ReadLineAsync().ConfigureAwait(false)) != null)
|
||||||
|
{
|
||||||
|
line += Environment.NewLine;
|
||||||
|
|
||||||
|
if (line.StartsWith('{'))
|
||||||
|
{
|
||||||
|
logger.LogInformation("OpenAI对话异常 , StatusCode: {StatusCode} Response: {Response}", response.StatusCode,
|
||||||
|
line);
|
||||||
|
|
||||||
|
throw new BusinessException("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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var result = JsonSerializer.Deserialize<ThorChatCompletionsResponse>(line,
|
||||||
|
ThorJsonSerializer.DefaultOptions);
|
||||||
|
|
||||||
|
if (result == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var content = result?.Choices?.FirstOrDefault()?.Delta;
|
||||||
|
|
||||||
|
if (first && content?.Content == OpenAIConstant.ThinkStart)
|
||||||
|
{
|
||||||
|
isThink = true;
|
||||||
|
continue;
|
||||||
|
// 需要将content的内容转换到其他字段
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isThink && content?.Content?.Contains(OpenAIConstant.ThinkEnd) == true)
|
||||||
|
{
|
||||||
|
isThink = false;
|
||||||
|
// 需要将content的内容转换到其他字段
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isThink && result?.Choices != null)
|
||||||
|
{
|
||||||
|
// 需要将content的内容转换到其他字段
|
||||||
|
foreach (var choice in result.Choices)
|
||||||
|
{
|
||||||
|
choice.Delta.ReasoningContent = choice.Delta.Content;
|
||||||
|
choice.Delta.Content = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
first = false;
|
||||||
|
|
||||||
|
yield return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ThorChatCompletionsResponse> CompleteChatAsync(AiModelDescribe options,
|
||||||
|
ThorChatCompletionsRequest chatCompletionCreate,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
using var openai =
|
||||||
|
Activity.Current?.Source.StartActivity("OpenAI 对话补全");
|
||||||
|
|
||||||
|
var response = await HttpClientFactory.GetHttpClient(options.Endpoint).PostJsonAsync(
|
||||||
|
options?.Endpoint.TrimEnd('/') + "/chat/completions",
|
||||||
|
chatCompletionCreate, options.ApiKey).ConfigureAwait(false);
|
||||||
|
|
||||||
|
openai?.SetTag("Model", chatCompletionCreate.Model);
|
||||||
|
openai?.SetTag("Response", response.StatusCode.ToString());
|
||||||
|
|
||||||
|
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("OpenAI对话异常 请求地址:{Address}, StatusCode: {StatusCode} Response: {Response}", options.Endpoint,
|
||||||
|
response.StatusCode, error);
|
||||||
|
|
||||||
|
throw new BusinessException("OpenAI对话异常", response.StatusCode.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
var result =
|
||||||
|
await response.Content.ReadFromJsonAsync<ThorChatCompletionsResponse>(
|
||||||
|
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Volo.Abp.DependencyInjection;
|
using Volo.Abp.DependencyInjection;
|
||||||
|
using Yi.Framework.AiHub.Domain.Shared.Dtos.Anthropic;
|
||||||
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
|
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
|
||||||
|
|
||||||
namespace Yi.Framework.AiHub.Domain.AiGateWay;
|
namespace Yi.Framework.AiHub.Domain.AiGateWay;
|
||||||
@@ -20,4 +21,12 @@ public class SpecialCompatible : ISpecialCompatible,ISingletonDependency
|
|||||||
handle(request);
|
handle(request);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void AnthropicCompatible(AnthropicInput request)
|
||||||
|
{
|
||||||
|
foreach (var handle in _options.Value.AnthropicHandles)
|
||||||
|
{
|
||||||
|
handle(request);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
|
using Yi.Framework.AiHub.Domain.Shared.Dtos.Anthropic;
|
||||||
|
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
|
||||||
|
|
||||||
namespace Yi.Framework.AiHub.Domain.AiGateWay;
|
namespace Yi.Framework.AiHub.Domain.AiGateWay;
|
||||||
|
|
||||||
public class SpecialCompatibleOptions
|
public class SpecialCompatibleOptions
|
||||||
{
|
{
|
||||||
public List<Action<ThorChatCompletionsRequest>> Handles { get; set; } = new();
|
public List<Action<ThorChatCompletionsRequest>> Handles { get; set; } = new();
|
||||||
|
public List<Action<AnthropicInput>> AnthropicHandles { get; set; } = new();
|
||||||
}
|
}
|
||||||
@@ -13,11 +13,14 @@ using Yi.Framework.AiHub.Domain.AiGateWay;
|
|||||||
using Yi.Framework.AiHub.Domain.AiGateWay.Exceptions;
|
using Yi.Framework.AiHub.Domain.AiGateWay.Exceptions;
|
||||||
using Yi.Framework.AiHub.Domain.Entities.Model;
|
using Yi.Framework.AiHub.Domain.Entities.Model;
|
||||||
using Yi.Framework.AiHub.Domain.Shared.Dtos;
|
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;
|
||||||
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi.Embeddings;
|
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi.Embeddings;
|
||||||
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi.Images;
|
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi.Images;
|
||||||
using Yi.Framework.Core.Extensions;
|
using Yi.Framework.Core.Extensions;
|
||||||
using Yi.Framework.SqlSugarCore.Abstractions;
|
using Yi.Framework.SqlSugarCore.Abstractions;
|
||||||
|
using JsonSerializer = System.Text.Json.JsonSerializer;
|
||||||
|
using ThorJsonSerializer = Yi.Framework.AiHub.Domain.AiGateWay.ThorJsonSerializer;
|
||||||
|
|
||||||
namespace Yi.Framework.AiHub.Domain.Managers;
|
namespace Yi.Framework.AiHub.Domain.Managers;
|
||||||
|
|
||||||
@@ -394,7 +397,7 @@ public class AiGateWayManager : DomainService
|
|||||||
|
|
||||||
var usage = new ThorUsageResponse()
|
var usage = new ThorUsageResponse()
|
||||||
{
|
{
|
||||||
PromptTokens = stream.Usage?.PromptTokens??0,
|
PromptTokens = stream.Usage?.PromptTokens ?? 0,
|
||||||
InputTokens = stream.Usage?.InputTokens ?? 0,
|
InputTokens = stream.Usage?.InputTokens ?? 0,
|
||||||
CompletionTokens = 0,
|
CompletionTokens = 0,
|
||||||
TotalTokens = stream.Usage?.InputTokens ?? 0
|
TotalTokens = stream.Usage?.InputTokens ?? 0
|
||||||
@@ -441,4 +444,210 @@ public class AiGateWayManager : DomainService
|
|||||||
throw new UserFriendlyException(errorContent);
|
throw new UserFriendlyException(errorContent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Anthropic聊天完成-流式
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request"></param>
|
||||||
|
/// <param name="cancellationToken"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async IAsyncEnumerable<(string, AnthropicStreamDto?)> AnthropicCompleteChatStreamAsync(
|
||||||
|
AnthropicInput request,
|
||||||
|
[EnumeratorCancellation] CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
_specialCompatible.AnthropicCompatible(request);
|
||||||
|
var modelDescribe = await GetModelAsync(request.Model);
|
||||||
|
var chatService =
|
||||||
|
LazyServiceProvider.GetRequiredKeyedService<IAnthropicChatCompletionService>(modelDescribe.HandlerName);
|
||||||
|
|
||||||
|
await foreach (var result in chatService.StreamChatCompletionsAsync(modelDescribe, request, cancellationToken))
|
||||||
|
{
|
||||||
|
yield return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Anthropic聊天完成-非流式
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="httpContext"></param>
|
||||||
|
/// <param name="request"></param>
|
||||||
|
/// <param name="userId"></param>
|
||||||
|
/// <param name="sessionId"></param>
|
||||||
|
/// <param name="cancellationToken"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task AnthropicCompleteChatForStatisticsAsync(HttpContext httpContext,
|
||||||
|
AnthropicInput request,
|
||||||
|
Guid? userId = null,
|
||||||
|
Guid? sessionId = null,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
_specialCompatible.AnthropicCompatible(request);
|
||||||
|
var response = httpContext.Response;
|
||||||
|
// 设置响应头,声明是 json
|
||||||
|
//response.ContentType = "application/json; charset=UTF-8";
|
||||||
|
var modelDescribe = await GetModelAsync(request.Model);
|
||||||
|
var chatService =
|
||||||
|
LazyServiceProvider.GetRequiredKeyedService<IAnthropicChatCompletionService>(modelDescribe.HandlerName);
|
||||||
|
var data = await chatService.ChatCompletionsAsync(modelDescribe, request, cancellationToken);
|
||||||
|
if (userId is not null)
|
||||||
|
{
|
||||||
|
await _aiMessageManager.CreateUserMessageAsync(userId.Value, sessionId,
|
||||||
|
new MessageInputDto
|
||||||
|
{
|
||||||
|
Content = request.Messages?.FirstOrDefault()?.Content ?? string.Empty,
|
||||||
|
ModelId = request.Model,
|
||||||
|
TokenUsage = data.TokenUsage,
|
||||||
|
});
|
||||||
|
|
||||||
|
await _aiMessageManager.CreateSystemMessageAsync(userId.Value, sessionId,
|
||||||
|
new MessageInputDto
|
||||||
|
{
|
||||||
|
Content = data.content?.FirstOrDefault()?.text,
|
||||||
|
ModelId = request.Model,
|
||||||
|
TokenUsage = data.TokenUsage
|
||||||
|
});
|
||||||
|
|
||||||
|
await _usageStatisticsManager.SetUsageAsync(userId.Value, request.Model, data.TokenUsage);
|
||||||
|
}
|
||||||
|
|
||||||
|
await response.WriteAsJsonAsync(data, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Anthropic聊天完成-缓存处理
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="httpContext"></param>
|
||||||
|
/// <param name="request"></param>
|
||||||
|
/// <param name="userId"></param>
|
||||||
|
/// <param name="sessionId"></param>
|
||||||
|
/// <param name="cancellationToken"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task AnthropicCompleteChatStreamForStatisticsAsync(
|
||||||
|
HttpContext httpContext,
|
||||||
|
AnthropicInput request,
|
||||||
|
Guid? userId = null,
|
||||||
|
Guid? sessionId = 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 gateWay = LazyServiceProvider.GetRequiredService<AiGateWayManager>();
|
||||||
|
var completeChatResponse = gateWay.AnthropicCompleteChatStreamAsync(request, cancellationToken);
|
||||||
|
ThorUsageResponse? tokenUsage = null;
|
||||||
|
StringBuilder backupSystemContent = new StringBuilder();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await foreach (var responseResult in completeChatResponse)
|
||||||
|
{
|
||||||
|
tokenUsage = responseResult.Item2?.TokenUsage;
|
||||||
|
backupSystemContent.Append(responseResult.Item2?.Delta?.Text);
|
||||||
|
await WriteAsEventStreamDataAsync(httpContext, responseResult.Item1, responseResult.Item2,
|
||||||
|
cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_logger.LogError(e, $"Ai对话异常");
|
||||||
|
var errorContent = $"对话Ai异常,异常信息:\n当前Ai模型:{request.Model}\n异常信息:{e.Message}\n异常堆栈:{e}";
|
||||||
|
var model = new AnthropicStreamDto
|
||||||
|
{
|
||||||
|
Message = new AnthropicChatCompletionDto
|
||||||
|
{
|
||||||
|
content =
|
||||||
|
[
|
||||||
|
new AnthropicChatCompletionDtoContent
|
||||||
|
{
|
||||||
|
text = errorContent,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
Error = new AnthropicStreamErrorDto
|
||||||
|
{
|
||||||
|
Type = null,
|
||||||
|
Message = errorContent
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var message = JsonConvert.SerializeObject(model, new JsonSerializerSettings
|
||||||
|
{
|
||||||
|
ContractResolver = new CamelCasePropertyNamesContractResolver()
|
||||||
|
});
|
||||||
|
await response.WriteAsJsonAsync(message, ThorJsonSerializer.DefaultOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
await _aiMessageManager.CreateUserMessageAsync(userId, sessionId,
|
||||||
|
new MessageInputDto
|
||||||
|
{
|
||||||
|
Content = request.Messages?.LastOrDefault()?.Content ?? string.Empty,
|
||||||
|
ModelId = request.Model,
|
||||||
|
TokenUsage = tokenUsage,
|
||||||
|
});
|
||||||
|
|
||||||
|
await _aiMessageManager.CreateSystemMessageAsync(userId, sessionId,
|
||||||
|
new MessageInputDto
|
||||||
|
{
|
||||||
|
Content = backupSystemContent.ToString(),
|
||||||
|
ModelId = request.Model,
|
||||||
|
TokenUsage = tokenUsage
|
||||||
|
});
|
||||||
|
|
||||||
|
await _usageStatisticsManager.SetUsageAsync(userId, request.Model, tokenUsage);
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Anthropic格式Http响应
|
||||||
|
|
||||||
|
private static readonly byte[] EventPrefix = "event: "u8.ToArray();
|
||||||
|
private static readonly byte[] DataPrefix = "data: "u8.ToArray();
|
||||||
|
private static readonly byte[] NewLine = "\n"u8.ToArray();
|
||||||
|
private static readonly byte[] DoubleNewLine = "\n\n"u8.ToArray();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 使用 JsonSerializer.SerializeAsync 直接序列化到响应流
|
||||||
|
/// </summary>
|
||||||
|
private static async ValueTask WriteAsEventStreamDataAsync<T>(
|
||||||
|
HttpContext context,
|
||||||
|
string @event,
|
||||||
|
T value,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
where T : class
|
||||||
|
{
|
||||||
|
var response = context.Response;
|
||||||
|
var bodyStream = response.Body;
|
||||||
|
// 确保 SSE Header 已经设置好
|
||||||
|
// e.g. Content-Type: text/event-stream; charset=utf-8
|
||||||
|
await response.StartAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
// 写事件类型
|
||||||
|
await bodyStream.WriteAsync(EventPrefix, cancellationToken).ConfigureAwait(false);
|
||||||
|
await WriteUtf8StringAsync(bodyStream, @event.Trim(), cancellationToken).ConfigureAwait(false);
|
||||||
|
await bodyStream.WriteAsync(NewLine, cancellationToken).ConfigureAwait(false);
|
||||||
|
// 写 data: + JSON
|
||||||
|
await bodyStream.WriteAsync(DataPrefix, cancellationToken).ConfigureAwait(false);
|
||||||
|
await JsonSerializer.SerializeAsync(
|
||||||
|
bodyStream,
|
||||||
|
value,
|
||||||
|
ThorJsonSerializer.DefaultOptions,
|
||||||
|
cancellationToken
|
||||||
|
).ConfigureAwait(false);
|
||||||
|
// 事件结束 \n\n
|
||||||
|
await bodyStream.WriteAsync(DoubleNewLine, cancellationToken).ConfigureAwait(false);
|
||||||
|
// 及时把数据发送给客户端
|
||||||
|
await bodyStream.FlushAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static async ValueTask WriteUtf8StringAsync(Stream stream, string value, CancellationToken token)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(value))
|
||||||
|
return;
|
||||||
|
var buffer = Encoding.UTF8.GetBytes(value);
|
||||||
|
await stream.WriteAsync(buffer, token).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
@@ -10,6 +10,8 @@ using Yi.Framework.AiHub.Domain.AiGateWay;
|
|||||||
using Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorAzureDatabricks.Chats;
|
using Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorAzureDatabricks.Chats;
|
||||||
using Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorAzureOpenAI.Chats;
|
using Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorAzureOpenAI.Chats;
|
||||||
using Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorAzureOpenAI.Images;
|
using Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorAzureOpenAI.Images;
|
||||||
|
using Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorClaude.Chats;
|
||||||
|
using Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorCustomOpenAI.Chats;
|
||||||
using Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorDeepSeek.Chats;
|
using Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorDeepSeek.Chats;
|
||||||
using Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorSiliconFlow.Embeddings;
|
using Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorSiliconFlow.Embeddings;
|
||||||
using Yi.Framework.AiHub.Domain.Managers.Fuwuhao;
|
using Yi.Framework.AiHub.Domain.Managers.Fuwuhao;
|
||||||
@@ -31,21 +33,43 @@ namespace Yi.Framework.AiHub.Domain
|
|||||||
var configuration = context.Services.GetConfiguration();
|
var configuration = context.Services.GetConfiguration();
|
||||||
var services = context.Services;
|
var services = context.Services;
|
||||||
|
|
||||||
// Configure<AiGateWayOptions>(configuration.GetSection("AiGateWay"));
|
#region OpenAi ChatCompletion
|
||||||
|
|
||||||
services.AddKeyedTransient<IChatCompletionService, AzureOpenAiChatCompletionCompletionsService>(
|
services.AddKeyedTransient<IChatCompletionService, AzureOpenAiChatCompletionCompletionsService>(
|
||||||
nameof(AzureOpenAiChatCompletionCompletionsService));
|
nameof(AzureOpenAiChatCompletionCompletionsService));
|
||||||
services.AddKeyedTransient<IChatCompletionService, AzureDatabricksChatCompletionsService>(
|
services.AddKeyedTransient<IChatCompletionService, AzureDatabricksChatCompletionsService>(
|
||||||
nameof(AzureDatabricksChatCompletionsService));
|
nameof(AzureDatabricksChatCompletionsService));
|
||||||
services.AddKeyedTransient<IChatCompletionService, DeepSeekChatCompletionsService>(
|
services.AddKeyedTransient<IChatCompletionService, DeepSeekChatCompletionsService>(
|
||||||
nameof(DeepSeekChatCompletionsService));
|
nameof(DeepSeekChatCompletionsService));
|
||||||
|
services.AddKeyedTransient<IChatCompletionService, OpenAiChatCompletionsService>(
|
||||||
|
nameof(OpenAiChatCompletionsService));
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Anthropic ChatCompletion
|
||||||
|
|
||||||
|
services.AddKeyedTransient<IAnthropicChatCompletionService, CustomOpenAIAnthropicChatCompletionsService>(
|
||||||
|
nameof(CustomOpenAIAnthropicChatCompletionsService));
|
||||||
|
services.AddKeyedTransient<IAnthropicChatCompletionService, AnthropicChatCompletionsService>(
|
||||||
|
nameof(AnthropicChatCompletionsService));
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
#region Image
|
||||||
|
|
||||||
services.AddKeyedTransient<IImageService, AzureOpenAIServiceImageService>(
|
services.AddKeyedTransient<IImageService, AzureOpenAIServiceImageService>(
|
||||||
nameof(AzureOpenAIServiceImageService));
|
nameof(AzureOpenAIServiceImageService));
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Embedding
|
||||||
|
|
||||||
services.AddKeyedTransient<ITextEmbeddingService, SiliconFlowTextEmbeddingService>(
|
services.AddKeyedTransient<ITextEmbeddingService, SiliconFlowTextEmbeddingService>(
|
||||||
nameof(SiliconFlowTextEmbeddingService));
|
nameof(SiliconFlowTextEmbeddingService));
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
//ai模型特殊性兼容处理
|
//ai模型特殊性兼容处理
|
||||||
Configure<SpecialCompatibleOptions>(options =>
|
Configure<SpecialCompatibleOptions>(options =>
|
||||||
{
|
{
|
||||||
@@ -82,8 +106,10 @@ namespace Yi.Framework.AiHub.Domain
|
|||||||
request.MaxTokens = 16384;
|
request.MaxTokens = 16384;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
options.AnthropicHandles.add(request => { });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
//配置支付宝支付
|
//配置支付宝支付
|
||||||
var config = configuration.GetSection("Alipay").Get<Config>();
|
var config = configuration.GetSection("Alipay").Get<Config>();
|
||||||
Factory.SetOptions(config);
|
Factory.SetOptions(config);
|
||||||
@@ -93,9 +119,5 @@ namespace Yi.Framework.AiHub.Domain
|
|||||||
|
|
||||||
services.AddHttpClient();
|
services.AddHttpClient();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task OnApplicationInitializationAsync(ApplicationInitializationContext context)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user