feat: 新增claude接口转换支持
This commit is contained in:
@@ -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; }
|
||||
|
||||
/// <summary>
|
||||
/// 参数验证
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="NotImplementedException"></exception>
|
||||
public IEnumerable<ValidationResult> Validate()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
[JsonPropertyName("enable_thinking")] public bool? EnableThinking { get; set; }
|
||||
|
||||
[JsonPropertyName("web_search_options")]
|
||||
public ThorChatWebSearchOptions? WebSearchOptions { get; set; } = null;
|
||||
|
||||
[JsonPropertyName("reasoning_effort")] public string? ReasoningEffort { get; set; }
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -25,56 +26,96 @@ public class ThorToolFunctionPropertyDefinition
|
||||
/// 表示字符串类型的函数对象
|
||||
/// </summary>
|
||||
String,
|
||||
|
||||
/// <summary>
|
||||
/// 表示整数类型的函数对象
|
||||
/// </summary>
|
||||
Integer,
|
||||
|
||||
/// <summary>
|
||||
/// 表示数字(包括浮点数等)类型的函数对象
|
||||
/// </summary>
|
||||
Number,
|
||||
|
||||
/// <summary>
|
||||
/// 表示对象类型的函数对象
|
||||
/// </summary>
|
||||
Object,
|
||||
|
||||
/// <summary>
|
||||
/// 表示数组类型的函数对象
|
||||
/// </summary>
|
||||
Array,
|
||||
|
||||
/// <summary>
|
||||
/// 表示布尔类型的函数对象
|
||||
/// </summary>
|
||||
Boolean,
|
||||
|
||||
/// <summary>
|
||||
/// 表示空值类型的函数对象
|
||||
/// </summary>
|
||||
Null
|
||||
}
|
||||
|
||||
public string typeStr = "object";
|
||||
|
||||
public string[] Types;
|
||||
|
||||
/// <summary>
|
||||
/// 必填的。函数参数对象类型。默认值为“object”。
|
||||
/// </summary>
|
||||
[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>
|
||||
[JsonPropertyName("properties")]
|
||||
public IDictionary<string, ThorToolFunctionPropertyDefinition?>? Properties { get; set; }
|
||||
public IDictionary<string, ThorToolFunctionPropertyDefinition>? Properties { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 可选。列出必需的“function arguments”列表。
|
||||
/// </summary>
|
||||
[JsonPropertyName("required")]
|
||||
public List<string>? Required { get; set; }
|
||||
public string[]? Required { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 可选。是否允许附加属性。默认值为true。
|
||||
/// </summary>
|
||||
[JsonPropertyName("additionalProperties")]
|
||||
public bool? AdditionalProperties { get; set; }
|
||||
public object? AdditionalProperties { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 可选。参数描述。
|
||||
@@ -219,11 +260,12 @@ public class ThorToolFunctionPropertyDefinition
|
||||
/// <param name="description"></param>
|
||||
/// <param name="enum"></param>
|
||||
/// <returns></returns>
|
||||
public static ThorToolFunctionPropertyDefinition DefineObject(IDictionary<string, ThorToolFunctionPropertyDefinition>? properties,
|
||||
List<string>? required,
|
||||
bool? additionalProperties,
|
||||
string? description,
|
||||
List<string>? @enum)
|
||||
public static ThorToolFunctionPropertyDefinition DefineObject(
|
||||
IDictionary<string, ThorToolFunctionPropertyDefinition>? properties,
|
||||
string[]? required,
|
||||
object? additionalProperties,
|
||||
string? description,
|
||||
List<string>? @enum)
|
||||
{
|
||||
return new ThorToolFunctionPropertyDefinition
|
||||
{
|
||||
@@ -242,7 +284,6 @@ public class ThorToolFunctionPropertyDefinition
|
||||
/// </summary>
|
||||
/// <param name="type">要转换的类型</param>
|
||||
/// <returns>给定类型的字符串表示形式</returns>
|
||||
|
||||
public static string ConvertTypeToString(FunctionObjectTypes type)
|
||||
{
|
||||
return type switch
|
||||
|
||||
@@ -4,5 +4,6 @@ public enum ModelTypeEnum
|
||||
{
|
||||
Chat = 0,
|
||||
Image = 1,
|
||||
Embedding = 2
|
||||
Embedding = 2,
|
||||
PremiumChat = 3
|
||||
}
|
||||
Reference in New Issue
Block a user