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(); foreach (var url in imageUrls) contentParts.Add(new { type = "image_url", image_url = new { url } }); if (!string.IsNullOrEmpty(userText)) contentParts.Add(new { type = "text", text = userText }); var userMessage = new ChatMessage { Role = "user", Content = JsonSerializer.Serialize(contentParts, _jsonOptions) }; messages.Add(userMessage); var request = new ChatCompletionRequest { Model = _model, Messages = messages, MaxTokens = maxTokens, Stream = false, }; 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)!; } } #region 请求/响应模型 public sealed class ChatCompletionRequest { public string Model { get; set; } = string.Empty; public List Messages { get; set; } = []; public bool Stream { get; set; } public int MaxTokens { get; set; } = 2048; public float Temperature { get; set; } = 0.7f; public float? TopP { get; set; } public List? Tools { get; set; } public string? ToolChoice { get; set; } } public sealed class ChatMessage { public string Role { get; set; } = string.Empty; public string Content { get; set; } = string.Empty; public string? ToolCallId { get; set; } public List? ToolCalls { get; set; } } public sealed class ToolDefinition { public string Type { get; set; } = "function"; public ToolFunction Function { get; set; } = new(); } public sealed class ToolFunction { public string Name { get; set; } = string.Empty; public string Description { get; set; } = string.Empty; public object Parameters { get; set; } = new(); } public sealed class ToolCall { public string Id { get; set; } = string.Empty; public string Type { get; set; } = "function"; public ToolCallFunction Function { get; set; } = new(); } public sealed class ToolCallFunction { public string Name { get; set; } = string.Empty; public string Arguments { get; set; } = string.Empty; } public sealed class ChatCompletionResponse { public string Id { get; set; } = string.Empty; public List Choices { get; set; } = []; } public sealed class Choice { public int Index { get; set; } public ResponseMessage? Message { get; set; } public ResponseDelta? Delta { get; set; } public string? FinishReason { get; set; } } public sealed class ResponseMessage { public string Role { get; set; } = string.Empty; public string? Content { get; set; } public List? ToolCalls { get; set; } } public sealed class ResponseDelta { public string? Content { get; set; } public string? Role { get; set; } } #endregion