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

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

View File

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