diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/OpenApiService.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/OpenApiService.cs
index 19c99e29..7c004ac6 100644
--- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/OpenApiService.cs
+++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/OpenApiService.cs
@@ -5,6 +5,7 @@ using Volo.Abp.Application.Services;
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.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;
@@ -117,6 +118,37 @@ public class OpenApiService : ApplicationService
};
}
+
+ ///
+ /// Anthropic对话
+ ///
+ ///
+ ///
+ [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)
{
// 获取Authorization头
@@ -138,4 +170,7 @@ public class OpenApiService : ApplicationService
throw new UserFriendlyException("当前海外站点不支持大流量接口,请使用转发站点:https://ai.ccnetcore.com");
}
}
+
+ #endregion
+
}
\ No newline at end of file
diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Dtos/Anthropic/AnthropicCacheControl.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Dtos/Anthropic/AnthropicCacheControl.cs
new file mode 100644
index 00000000..82dd0f23
--- /dev/null
+++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Dtos/Anthropic/AnthropicCacheControl.cs
@@ -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; }
+}
\ No newline at end of file
diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Dtos/Anthropic/AnthropicChatCompletionDto.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Dtos/Anthropic/AnthropicChatCompletionDto.cs
new file mode 100644
index 00000000..5e9fa1b3
--- /dev/null
+++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Dtos/Anthropic/AnthropicChatCompletionDto.cs
@@ -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; }
+}
\ No newline at end of file
diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Dtos/Anthropic/AnthropicInput.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Dtos/Anthropic/AnthropicInput.cs
new file mode 100644
index 00000000..49d3c2d7
--- /dev/null
+++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Dtos/Anthropic/AnthropicInput.cs
@@ -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 Messages { get; set; }
+
+ [JsonPropertyName("tools")] public IList? 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(ThorJsonSerializer.DefaultOptions);
+ }
+ }
+ else
+ {
+ ToolChoice = (AnthropicTooChoiceInput)value;
+ }
+ }
+ }
+
+ [JsonIgnore] public string? ToolChoiceString { get; set; }
+
+ [JsonIgnore] public AnthropicTooChoiceInput? ToolChoice { get; set; }
+
+ [JsonIgnore] public IList? 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>(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? 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? 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; }
+}
\ No newline at end of file
diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Dtos/Anthropic/AnthropicMessageContent.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Dtos/Anthropic/AnthropicMessageContent.cs
new file mode 100644
index 00000000..ae6ecc04
--- /dev/null
+++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Dtos/Anthropic/AnthropicMessageContent.cs
@@ -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>(value?.ToString());
+ }
+ }
+ else
+ {
+ _content = value?.ToString();
+ }
+ }
+ }
+
+ private string? _content;
+
+ private List? _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; }
+}
\ No newline at end of file
diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Dtos/Anthropic/AnthropicMessageInput.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Dtos/Anthropic/AnthropicMessageInput.cs
new file mode 100644
index 00000000..8242f251
--- /dev/null
+++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Dtos/Anthropic/AnthropicMessageInput.cs
@@ -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>(value?.ToString(),ThorJsonSerializer.DefaultOptions);
+ }
+ }
+ else
+ {
+ Content = value?.ToString();
+ }
+ }
+ }
+
+ [JsonIgnore]
+ public IList? Contents;
+}
\ No newline at end of file
diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Dtos/Anthropic/AnthropicToOpenAI.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Dtos/Anthropic/AnthropicToOpenAI.cs
new file mode 100644
index 00000000..9f925f9e
--- /dev/null
+++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Dtos/Anthropic/AnthropicToOpenAI.cs
@@ -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
+{
+ ///
+ /// 将AnthropicInput转换为ThorChatCompletionsRequest
+ ///
+ public static ThorChatCompletionsRequest ConvertAnthropicToOpenAi(AnthropicInput anthropicInput)
+ {
+ var openAiRequest = new ThorChatCompletionsRequest
+ {
+ Model = anthropicInput.Model,
+ MaxTokens = anthropicInput.MaxTokens,
+ Stream = anthropicInput.Stream,
+ Messages = new List(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;
+ }
+
+ ///
+ /// 根据最后的内容块类型和OpenAI的完成原因确定Claude的停止原因
+ ///
+ public static string GetStopReasonByLastContentType(string? openAiFinishReason, string lastContentBlockType)
+ {
+ // 如果最后一个内容块是工具调用,优先返回tool_use
+ if (lastContentBlockType == "tool_use")
+ {
+ return "tool_use";
+ }
+
+ // 否则使用标准的转换逻辑
+ return GetClaudeStopReason(openAiFinishReason);
+ }
+
+ ///
+ /// 创建message_start事件
+ ///
+ 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
+ }
+ }
+ };
+ }
+
+ ///
+ /// 创建content_block_start事件
+ ///
+ public static AnthropicStreamDto CreateContentBlockStartEvent()
+ {
+ return new AnthropicStreamDto
+ {
+ Type = "content_block_start",
+ Index = 0,
+ ContentBlock = new AnthropicChatCompletionDtoContentBlock
+ {
+ Type = "text",
+ Id = null,
+ Name = null
+ }
+ };
+ }
+
+ ///
+ /// 创建thinking block start事件
+ ///
+ public static AnthropicStreamDto CreateThinkingBlockStartEvent()
+ {
+ return new AnthropicStreamDto
+ {
+ Type = "content_block_start",
+ Index = 0,
+ ContentBlock = new AnthropicChatCompletionDtoContentBlock
+ {
+ Type = "thinking",
+ Id = null,
+ Name = null
+ }
+ };
+ }
+
+ ///
+ /// 创建content_block_delta事件
+ ///
+ public static AnthropicStreamDto CreateContentBlockDeltaEvent(string text)
+ {
+ return new AnthropicStreamDto
+ {
+ Type = "content_block_delta",
+ Index = 0,
+ Delta = new AnthropicChatCompletionDtoDelta
+ {
+ Type = "text_delta",
+ Text = text
+ }
+ };
+ }
+
+ ///
+ /// 创建thinking delta事件
+ ///
+ public static AnthropicStreamDto CreateThinkingBlockDeltaEvent(string thinking)
+ {
+ return new AnthropicStreamDto
+ {
+ Type = "content_block_delta",
+ Index = 0,
+ Delta = new AnthropicChatCompletionDtoDelta
+ {
+ Type = "thinking",
+ Thinking = thinking
+ }
+ };
+ }
+
+ ///
+ /// 创建content_block_stop事件
+ ///
+ public static AnthropicStreamDto CreateContentBlockStopEvent()
+ {
+ return new AnthropicStreamDto
+ {
+ Type = "content_block_stop",
+ Index = 0
+ };
+ }
+
+ ///
+ /// 创建message_delta事件
+ ///
+ public static AnthropicStreamDto CreateMessageDeltaEvent(string finishReason, AnthropicCompletionDtoUsage usage)
+ {
+ return new AnthropicStreamDto
+ {
+ Type = "message_delta",
+ Usage = usage,
+ Delta = new AnthropicChatCompletionDtoDelta
+ {
+ StopReason = finishReason
+ }
+ };
+ }
+
+ ///
+ /// 创建message_stop事件
+ ///
+ public static AnthropicStreamDto CreateMessageStopEvent()
+ {
+ return new AnthropicStreamDto
+ {
+ Type = "message_stop"
+ };
+ }
+
+ ///
+ /// 创建tool block start事件
+ ///
+ 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
+ }
+ };
+ }
+
+ ///
+ /// 创建tool delta事件
+ ///
+ public static AnthropicStreamDto CreateToolBlockDeltaEvent(string partialJson)
+ {
+ return new AnthropicStreamDto
+ {
+ Type = "content_block_delta",
+ Index = 0,
+ Delta = new AnthropicChatCompletionDtoDelta
+ {
+ Type = "input_json_delta",
+ PartialJson = partialJson
+ }
+ };
+ }
+
+ ///
+ /// 转换Anthropic消息为Thor消息列表
+ ///
+ public static List ConvertAnthropicMessageToThor(AnthropicMessageInput anthropicMessage)
+ {
+ var results = new List();
+
+ // 处理简单的字符串内容
+ 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();
+ var currentToolCalls = new List();
+
+ 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();
+ }
+
+ // 收集工具调用
+ 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();
+ }
+
+ // 创建工具结果消息
+ 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;
+ }
+
+ ///
+ /// 转换Anthropic工具为Thor工具
+ ///
+ public static ThorToolDefinition ConvertAnthropicToolToThor(AnthropicMessageTool anthropicTool)
+ {
+ IDictionary values =
+ new Dictionary();
+
+ 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
+ }
+ }
+ };
+ }
+
+ ///
+ /// 将OpenAI的完成原因转换为Claude的停止原因
+ ///
+ 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"
+ };
+ }
+
+ ///
+ /// 将OpenAI响应转换为Claude响应格式
+ ///
+ 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();
+
+ 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