using System.Net.Http.Headers;
namespace Health.Infrastructure.AI;
///
/// OpenAI 兼容协议 HTTP 客户端,统一调用 DeepSeek / 千问 VL
///
public sealed class OpenAiCompatibleClient(string baseUrl, string apiKey, string model)
{
private readonly HttpClient _http = CreateHttpClient(baseUrl, apiKey);
private readonly string _model = model;
private readonly JsonSerializerOptions _jsonOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
PropertyNameCaseInsensitive = true
};
private static HttpClient CreateHttpClient(string baseUrl, string apiKey)
{
var client = new HttpClient
{
BaseAddress = new Uri(baseUrl.TrimEnd('/') + "/"),
Timeout = TimeSpan.FromSeconds(60)
};
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
return client;
}
///
/// 流式 Chat Completions(SSE)
///
public async IAsyncEnumerable ChatStreamAsync(
List messages,
List? tools = null,
int maxTokens = 2048,
float temperature = 0.7f)
{
var request = new ChatCompletionRequest
{
Model = _model,
Messages = messages,
Stream = true,
MaxTokens = maxTokens,
Temperature = temperature,
Tools = tools,
};
if (tools?.Count > 0)
request.ToolChoice = "auto";
var json = JsonSerializer.Serialize(request, _jsonOptions);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var httpRequest = new HttpRequestMessage(HttpMethod.Post, "chat/completions")
{
Content = content
};
httpRequest.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/event-stream"));
var response = await _http.SendAsync(httpRequest, HttpCompletionOption.ResponseHeadersRead);
response.EnsureSuccessStatusCode();
using var stream = await response.Content.ReadAsStreamAsync();
using var reader = new StreamReader(stream);
string? line;
while ((line = await reader.ReadLineAsync()) != null)
{
if (string.IsNullOrWhiteSpace(line)) continue;
if (!line.StartsWith("data: ")) continue;
var data = line["data: ".Length..];
if (data == "[DONE]") break;
yield return data;
}
}
///
/// 非流式 Chat Completions
///
public async Task ChatAsync(
List messages,
List? tools = null,
int maxTokens = 2048,
float temperature = 0.7f)
{
var request = new ChatCompletionRequest
{
Model = _model,
Messages = messages,
Stream = false,
MaxTokens = maxTokens,
Temperature = temperature,
Tools = tools,
};
if (tools?.Count > 0)
request.ToolChoice = "auto";
var json = JsonSerializer.Serialize(request, _jsonOptions);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await _http.PostAsync("chat/completions", content);
response.EnsureSuccessStatusCode();
var body = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize(body, _jsonOptions)!;
}
///
/// Vision 图片理解(非流式)
///
public async Task VisionAsync(
string systemPrompt,
List imageUrls,
string? userText = null,
int maxTokens = 2048)
{
var messages = new List();
if (!string.IsNullOrEmpty(systemPrompt))
messages.Add(new ChatMessage { Role = "system", Content = systemPrompt });
// 构建多模态消息内容
var contentParts = new List